@@ -305,6 +305,208 @@ GET /api/restify/users?sort=-created_at # Descending
305305GET /api/restify/users?sort=name,-created_at # Multiple fields
306306```
307307
308+ ## Matching
309+
310+ Fields can be made matchable, allowing API consumers to filter results using query parameters.
311+
312+ ### Making Fields Matchable
313+
314+ Use the ` matchable() ` method or convenient aliases:
315+
316+ ``` php
317+ public function fields(RestifyRequest $request)
318+ {
319+ return [
320+ field('name')->matchableText(), // Text matching with LIKE
321+ field('email')->matchable('users.email'), // Custom column - users table email
322+ field('status')->matchableText(), // Text matching
323+ field('is_active')->matchableBool(), // Boolean matching
324+ field('age')->matchableInteger(), // Integer matching
325+ field('created_at')->matchableDatetime(), // Date matching
326+ field('price')->matchableBetween(), // Range matching
327+ field('tags')->matchableArray(), // Array/IN matching
328+ ];
329+ }
330+ ```
331+
332+ ### Using Matchable Fields
333+
334+ Once fields are marked as matchable, API consumers can filter using query parameters:
335+
336+ ``` http
337+ GET /api/restify/posts?title=Laravel # Text matching
338+ GET /api/restify/posts?is_active=true # Boolean matching
339+ GET /api/restify/posts?user_id=5 # Integer matching
340+ GET /api/restify/posts?created_at=2023-12-01 # Date matching
341+ GET /api/restify/posts?price=100,500 # Range matching
342+ GET /api/restify/posts?tags=php,laravel # Array matching
343+
344+ # Negation (prefix with -)
345+ GET /api/restify/posts?-status=draft # Exclude drafts
346+ GET /api/restify/posts?-is_active=true # Inactive posts
347+
348+ # Null checks
349+ GET /api/restify/posts?description=null # Posts with no description
350+ ```
351+
352+ ### Match Types Reference
353+
354+ | Alias | Type | Example Usage | Query Behavior |
355+ | -------| ------| ---------------| ----------------|
356+ | ` matchableText() ` | text | ` ?name=john ` | ` WHERE name LIKE '%john%' ` |
357+ | ` matchableBool() ` | boolean | ` ?is_active=true ` | ` WHERE is_active = 1 ` |
358+ | ` matchableInteger() ` | integer | ` ?user_id=5 ` | ` WHERE user_id = 5 ` |
359+ | ` matchableDatetime() ` | datetime | ` ?created_at=2023-12-01 ` | ` WHERE DATE(created_at) = '2023-12-01' ` |
360+ | ` matchableBetween() ` | between | ` ?price=100,500 ` | ` WHERE price BETWEEN 100 AND 500 ` |
361+ | ` matchableArray() ` | array | ` ?tags=php,laravel ` | ` WHERE tags IN ('php', 'laravel') ` |
362+
363+ ### Advanced Matchable Configuration
364+
365+ The ` matchable() ` method is flexible and accepts multiple types of arguments for advanced filtering scenarios:
366+
367+ #### Basic Usage (No Arguments)
368+
369+ When called without arguments, ` matchable() ` enables text-based matching using the field's attribute name:
370+
371+ ``` php
372+ field('title')->matchable(), // Enables text matching on 'title' column
373+ ```
374+
375+ #### Custom Column
376+
377+ Specify a different database column for matching:
378+
379+ ``` php
380+ field('display_name')->matchable('users.name'), // Match against 'users.name' column
381+ ```
382+
383+ #### Custom Match Type
384+
385+ Specify both column and match type:
386+
387+ ``` php
388+ field('status')->matchable('posts.status', 'text'), // Custom column with text matching
389+ field('priority')->matchable('priority', 'integer'), // Integer matching
390+ ```
391+
392+ #### Closure-based Matching
393+
394+ For complex filtering logic, pass a closure that receives the request, query builder, and value:
395+
396+ ``` php
397+ field('title')->matchable(function ($request, $query, $value) {
398+ // Custom search logic - case insensitive partial matching
399+ $query->where('title', 'like', "%{$value}%");
400+ }),
401+
402+ field('content')->matchable(function ($request, $query, $value) {
403+ // Full-text search across multiple columns
404+ $query->whereRaw("MATCH(title, content) AGAINST(? IN BOOLEAN MODE)", [$value]);
405+ }),
406+
407+ field('location')->matchable(function ($request, $query, $value) {
408+ // Complex geographical search
409+ [$lat, $lng, $radius] = explode(',', $value);
410+ $query->whereRaw(
411+ 'ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) <= ?',
412+ [$lng, $lat, $radius * 1000]
413+ );
414+ }),
415+ ```
416+
417+ #### Custom MatchFilter Classes
418+
419+ For reusable complex filtering logic, create custom MatchFilter classes:
420+
421+ ``` php
422+ use Binaryk\LaravelRestify\Filters\MatchFilter;
423+ use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
424+ use Illuminate\Database\Eloquent\Builder;
425+ use Illuminate\Database\Eloquent\Relations\Relation;
426+
427+ class CustomTitleFilter extends MatchFilter
428+ {
429+ public function __construct()
430+ {
431+ parent::__construct();
432+ $this->setColumn('title'); // Set the column to filter on
433+ }
434+
435+ public function filter(RestifyRequest $request, Builder|Relation $query, $value)
436+ {
437+ // Custom filtering logic: search for titles that start with the given value
438+ $query->where('title', 'like', "{$value}%");
439+
440+ return $query;
441+ }
442+ }
443+ ```
444+
445+ Then use the custom filter in your field definition:
446+
447+ ``` php
448+ field('title')->matchable(new CustomTitleFilter()),
449+ ```
450+
451+ #### Invokable Classes
452+
453+ You can also use invokable classes for cleaner code organization:
454+
455+ ``` php
456+ class SearchTitleFilter
457+ {
458+ public function __invoke($request, $query, $value)
459+ {
460+ $query->where('title', 'like', "%{$value}%")
461+ ->orWhere('description', 'like', "%{$value}%");
462+ }
463+ }
464+
465+ // Usage
466+ field('search')->matchable(new SearchTitleFilter()),
467+ ```
468+
469+ #### Practical Examples
470+
471+ ** E-commerce Product Search:**
472+ ``` php
473+ field('search')->matchable(function ($request, $query, $value) {
474+ $query->where(function ($q) use ($value) {
475+ $q->where('name', 'like', "%{$value}%")
476+ ->orWhere('description', 'like', "%{$value}%")
477+ ->orWhere('sku', 'like', "%{$value}%");
478+ });
479+ }),
480+ ```
481+
482+ ** Date Range Filtering:**
483+ ``` php
484+ field('date_range')->matchable(function ($request, $query, $value) {
485+ [$start, $end] = explode(',', $value);
486+ $query->whereBetween('created_at', [$start, $end]);
487+ }),
488+ ```
489+
490+ ** Tag-based Filtering:**
491+ ``` php
492+ field('tags')->matchable(function ($request, $query, $value) {
493+ $tags = explode(',', $value);
494+ $query->whereHas('tags', function ($q) use ($tags) {
495+ $q->whereIn('slug', $tags);
496+ });
497+ }),
498+ ```
499+
500+ ** Relationship Filtering:**
501+ ``` php
502+ field('author')->matchable(function ($request, $query, $value) {
503+ $query->whereHas('author', function ($q) use ($value) {
504+ $q->where('name', 'like', "%{$value}%")
505+ ->orWhere('email', 'like', "%{$value}%");
506+ });
507+ }),
508+ ```
509+
308510## Validation
309511
310512There is a golden rule that says - catch the exception as soon as possible on its request way.
0 commit comments