Skip to content

Commit 93f29c1

Browse files
committed
fix: searchables
1 parent 3f8ce3b commit 93f29c1

File tree

9 files changed

+556
-123
lines changed

9 files changed

+556
-123
lines changed

docs-v2/content/en/api/fields.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,196 @@ field('author')->matchable(function ($request, $query, $value) {
507507
}),
508508
```
509509

510+
## Searchable
511+
512+
Fields can be made searchable, enabling them to respond to global search queries. This provides field-level control over search behavior while maintaining the simplicity of the global search API.
513+
514+
### Making Fields Searchable
515+
516+
To make a field searchable, chain the `searchable()` method:
517+
518+
```php
519+
public function fields(RestifyRequest $request)
520+
{
521+
return [
522+
field('title')->searchable(),
523+
field('description')->searchable(),
524+
field('email')->searchable(),
525+
];
526+
}
527+
```
528+
529+
The `searchable()` method uses a unified flexible signature that accepts multiple arguments and works consistently across all field types:
530+
531+
```php
532+
// Basic usage
533+
field('title')->searchable(),
534+
535+
// Custom column
536+
field('name')->searchable('users.full_name'),
537+
538+
// With optional type
539+
field('price')->searchable('products.price', 'numeric'),
540+
541+
// Multiple attributes (especially useful for relationship fields like BelongsTo)
542+
BelongsTo::make('author')->searchable('name', 'email', 'username'),
543+
544+
// Array of attributes (legacy support)
545+
BelongsTo::make('editor')->searchable(['users.name', 'users.email']),
546+
547+
// Closure/callback
548+
field('content')->searchable(function ($request, $query, $value) {
549+
// Custom search logic
550+
}),
551+
552+
// Custom filter instance
553+
field('complex_search')->searchable(new CustomSearchFilter()),
554+
555+
// Invokable class
556+
field('tags')->searchable(new TagSearchHandler()),
557+
```
558+
559+
### Unified Method Signatures
560+
561+
All searchable-related methods now use consistent signatures across regular fields and relationship fields:
562+
563+
```php
564+
// All field types use the same signatures:
565+
searchable(...$attributes) // Flexible variadic signature
566+
isSearchable(?RestifyRequest $request = null) // Optional request parameter
567+
getSearchColumn(?RestifyRequest $request = null) // Optional request parameter
568+
569+
// BelongsTo also provides relationship-specific method:
570+
getSearchables(): array // Returns multiple searchable attributes
571+
```
572+
573+
### Using Searchable Fields
574+
575+
Searchable fields respond to the standard `search` query parameter:
576+
577+
```http
578+
GET /api/restify/posts?search=laravel
579+
```
580+
581+
This will search across all searchable fields for the term "laravel".
582+
583+
### Advanced Searchable Configuration
584+
585+
#### Basic Usage (No Arguments)
586+
587+
When called without arguments, `searchable()` applies standard search behavior using the field's attribute:
588+
589+
```php
590+
field('title')->searchable(), // Searches the 'title' column with LIKE operator
591+
```
592+
593+
#### Custom Column
594+
595+
Specify a different database column for searching:
596+
597+
```php
598+
field('author_name')->searchable('users.name'), // Search in users.name column
599+
```
600+
601+
You can also specify multiple attributes for relationship fields (like BelongsTo):
602+
603+
```php
604+
BelongsTo::make('author', UserRepository::class)->searchable('name', 'email'),
605+
```
606+
607+
#### Closure-based Searching
608+
609+
For custom search logic, pass a closure that receives the request, query builder, and search value:
610+
611+
```php
612+
field('content')->searchable(function ($request, $query, $value) {
613+
$query->where('title', 'LIKE', "%{$value}%")
614+
->orWhere('description', 'LIKE', "%{$value}%");
615+
}),
616+
```
617+
618+
#### Custom SearchableFilter Classes
619+
620+
Create dedicated filter classes for complex search logic:
621+
622+
```php
623+
field('complex_search')->searchable(new CustomContentSearchFilter),
624+
```
625+
626+
Where `CustomContentSearchFilter` extends `SearchableFilter`:
627+
628+
```php
629+
use Binaryk\LaravelRestify\Filters\SearchableFilter;
630+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
631+
632+
class CustomContentSearchFilter extends SearchableFilter
633+
{
634+
public function filter(RestifyRequest $request, $query, $value)
635+
{
636+
return $query->where(function ($q) use ($value) {
637+
$q->where('title', 'LIKE', "%{$value}%")
638+
->orWhere('description', 'LIKE', "%{$value}%")
639+
->orWhere('tags', 'LIKE', "%{$value}%");
640+
});
641+
}
642+
}
643+
```
644+
645+
#### Invokable Classes
646+
647+
For reusable search logic, use invokable classes:
648+
649+
```php
650+
field('tags')->searchable(new TagSearchFilter),
651+
```
652+
653+
```php
654+
class TagSearchFilter
655+
{
656+
public function __invoke($request, $query, $value)
657+
{
658+
$tags = explode(',', $value);
659+
$query->whereHas('tags', function ($q) use ($tags) {
660+
$q->whereIn('name', $tags);
661+
});
662+
}
663+
}
664+
```
665+
666+
#### Practical Examples
667+
668+
**Full-text Search:**
669+
```php
670+
field('content')->searchable(function ($request, $query, $value) {
671+
$query->whereFullText(['title', 'description'], $value);
672+
}),
673+
```
674+
675+
**Multi-field Search:**
676+
```php
677+
field('user_search')->searchable(function ($request, $query, $value) {
678+
$query->where('name', 'LIKE', "%{$value}%")
679+
->orWhere('email', 'LIKE', "%{$value}%")
680+
->orWhere('phone', 'LIKE', "%{$value}%");
681+
}),
682+
```
683+
684+
**Relationship Search:**
685+
```php
686+
field('author')->searchable(function ($request, $query, $value) {
687+
$query->whereHas('author', function ($q) use ($value) {
688+
$q->where('name', 'like', "%{$value}%");
689+
});
690+
}),
691+
```
692+
693+
**JSON Search:**
694+
```php
695+
field('metadata')->searchable(function ($request, $query, $value) {
696+
$query->whereJsonContains('metadata->tags', $value);
697+
}),
698+
```
699+
510700
## Validation
511701

512702
There is a golden rule that says - catch the exception as soon as possible on its request way.

src/Fields/BelongsTo.php

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,67 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n
4242
);
4343
}
4444

45+
/**
46+
* Override the parent searchable method to handle BelongsTo-specific multiple attributes
47+
*/
4548
public function searchable(...$attributes): self
4649
{
47-
$this->searchablesAttributes = collect($attributes)->flatten()->all();
50+
// Handle case where a single array is passed (legacy behavior)
51+
if (count($attributes) === 1 && is_array($attributes[0])) {
52+
$this->searchablesAttributes = collect($attributes[0])->flatten()->all();
53+
// Also call parent with the first attribute for consistency
54+
if (!empty($this->searchablesAttributes)) {
55+
parent::searchable($this->searchablesAttributes[0]);
56+
}
57+
return $this;
58+
}
59+
60+
// If it's relationship-specific multiple attributes (all strings), use BelongsTo behavior
61+
if (count($attributes) > 1 && collect($attributes)->every(fn($attr) => is_string($attr))) {
62+
$this->searchablesAttributes = collect($attributes)->flatten()->all();
63+
// Also call parent to maintain consistency with CanSearch trait
64+
parent::searchable($attributes[0]);
65+
return $this;
66+
}
67+
68+
// For single attribute or complex cases (closures, filters), use parent behavior
69+
parent::searchable(...$attributes);
70+
71+
// If parent set a simple string column, also set it in searchablesAttributes for consistency
72+
if (count($attributes) === 1 && is_string($attributes[0])) {
73+
$this->searchablesAttributes = [$attributes[0]];
74+
}
4875

4976
return $this;
5077
}
5178

52-
public function isSearchable(): bool
79+
/**
80+
* Check if this BelongsTo field is searchable (either via attributes or parent CanSearch)
81+
*/
82+
public function isSearchable(?RestifyRequest $request = null): bool
5383
{
54-
return ! is_null($this->searchablesAttributes);
84+
return ! is_null($this->searchablesAttributes) || parent::isSearchable($request);
5585
}
5686

87+
/**
88+
* Get the searchable attributes specific to BelongsTo relationships
89+
*/
5790
public function getSearchables(): array
5891
{
59-
return $this->searchablesAttributes;
92+
return $this->searchablesAttributes ?? [];
93+
}
94+
95+
/**
96+
* Override parent getSearchColumn to provide BelongsTo-specific behavior
97+
*/
98+
public function getSearchColumn(?RestifyRequest $request = null): mixed
99+
{
100+
// If we have BelongsTo-specific attributes, return the first one for compatibility
101+
if (!empty($this->searchablesAttributes)) {
102+
return $this->searchablesAttributes[0];
103+
}
104+
105+
// Otherwise, use parent behavior
106+
return parent::getSearchColumn($request);
60107
}
61108
}

0 commit comments

Comments
 (0)