bayareawebpro / searchable-resource
Searchable Resource Builder for Laravel
Requires
- php: ^7.4|^8.0
- laravel/framework: ^8.0|^9.0
Requires (Dev)
- laravel/legacy-factories: ^1.0
- nunomaduro/larastan: ^0.5|^1.0|^2.0
- orchestra/testbench: ^4.0|^5.0|^6.0|^7.0
- phpunit/phpunit: ^8.0|^9.0
This package is auto-updated.
Last update: 2023-10-09 04:18:37 UTC
README
Searchable Resource Builder is an abstraction for building searchable resource responses in Laravel applications. Extract query logic into reusable chunks while using a fluent builder interface for dealing with searchable / filterable / sortable requests and JSON / API Resources.
composer require bayareawebpro/searchable-resource
Basic Usage
SearchableResources implement the Responsable
interface which allows them to be
returned from controllers easily. It can also be used with blade.
The make
method accepts instances of Eloquent Builder.
SearchableResource::make(User::query());
Ordering and Sorting
You can specify as many orderable columns as you wish.
SearchableResource::make(User::query()) ->orderable(['name', 'email']) ->orderBy('name') ->sort('desc') ->paginate(16);
The default settings:
- order_by ID
- sort DESC
Full Example
use App\User; use App\Queries\UserSearch; use App\Queries\RoleFilter; use App\Http\Resources\UserResource; use BayAreaWebPro\SearchableResource\SearchableResource; use BayAreaWebPro\SearchableResource\SearchableBuilder; SearchableResource::make(User::query()) ->resource(UserResource::class) ->queries([ UserSearch::class, RoleFilter::class ]) ->orderable([ 'id', 'name', 'email', 'role', 'created_at', 'updated_at', ]) ->appendable([ 'created_for_humans', 'updated_for_humans', ]) ->select([ 'id', 'name', 'email', 'role', 'created_at', 'updated_at', ]) ->rules([ 'my_filter_key' => 'required|string' ]) ->params([ 'my_filter_key' => 'my_default' ]) ->options([ 'my_filter_key' => ['my_default', 'option2', 'option3'] ]) ->with([ 'my_key' => true ]) ->when(true, fn(SearchableBuilder $builder)=>$builder ->with([ 'my_key' => false ]) ) ->orderBy('updated_at') ->sort('desc') ->paginate(16) ->labeled();
Blade / View Example
Execute the query and return a view with the items and options.
public function index() { $resource = SearchableResource::make(User::query()) ->query(Users::make()) ->orderable(['name', 'email']) ->orderBy('name') ->sort('desc') ->paginate(5) ->execute() ; return view('users.index', [ 'items' =>$resource->getItems(), 'search' =>$resource->getSearch(), 'order_by' =>$resource->getOrderBy(), 'per_page' =>$resource->getPerPage(), 'options' =>$resource->getOptions(), 'sort' =>$resource->getSort(), ]); }
<x-admin::form method="GET"> <x-admin::search name="search" value="{{ $search ?? null }}" /> <x-admin::select name="order_by" value="{{ $order_by ?? null }}" :options="$options->get('order_by')" /> <x-admin::select name="sort" value="{{ $sort ?? null }}" :options="$options->get('sort')" /> <x-admin::select name="per_page" value="{{ $per_page ?? null }}" :options="$options->get('per_page')" /> <x-admin::submit label="{{ __('resources.filter') }}" /> </x-admin::form>
JSON Resources
SearchableResources are generic JsonResources by default. You can easily specify which resource class should be used to map your models when building the response.
Must extend
JsonResource
.
SearchableResource::make(User::query())->resource(UserResource::class);
Invokable Queries
Queries are expressed as invokable classes that extend the AbstractQuery
class
which contains logic per request field. Queries can apply to multiple attributes/columns
as well as multiple inputs for orWhere
clauses.
php artisan make:searchable NameQuery
The following is an example of a generic name query:
<?php declare(strict_types=1); namespace App\Queries; use Illuminate\Database\Eloquent\Builder; use BayAreaWebPro\SearchableResource\AbstractQuery; class LikeQuery extends AbstractQuery { public string $field = 'search'; protected string $attribute = 'name'; public function __invoke(Builder $builder): void { $builder->where($this->attribute, "like", "%{$this->getValue($this->field)}%"); } }
SearchableResource::make(User::query()) ->query( LikeQuery::make() ->field('search') ->attribute('last_name') ) ->query( SelectQuery::make() ->field('role') ->attribute('role') ->options(['admin', 'customer']) ) ;
ConditionalQuery Contract
Queries that implement the ConditionalQuery
Contract will only be applied when
their applies
method returns true
.
By default an query that extends AbstractQuery
class using the ConditionalQuery
contract
already implements this method for you by calling the filled
method on the request.
Override the parent method to customize.
<?php declare(strict_types=1); namespace App\Queries; use BayAreaWebPro\SearchableResource\AbstractQuery; use BayAreaWebPro\SearchableResource\Contracts\ConditionalQuery; class ConditionalRoleQuery extends AbstractQuery implements ConditionalQuery { public string $field = 'role'; protected string $attribute = 'role'; public function __invoke(Builder $builder): void { $builder->where($this->attribute, $this->getValue($this->field)); } public function getApplies(): bool { return parent::getApplies(); // Customize with $this->request } }
Validation
Queries can specify their own validation rules by implementing the ValidatableQuery
contract to be merged the rules for the searchable collection.
ValidatableQuery Contract
Queries that implement the ValidatableQuery
Contract will have their returned rules
merged into the validator, otherwise the rules will be ignored.
<?php declare(strict_types=1); namespace App\Queries; use BayAreaWebPro\SearchableResource\AbstractQuery; use BayAreaWebPro\SearchableResource\Contracts\ValidatableQuery; class ConditionalRoleQuery extends AbstractQuery implements ValidatableQuery { public string $role = 'role'; public string $admins = 'only_admins'; protected string $attribute = 'role'; public function __invoke(Builder $builder): void { $builder->where($this->attribute, $this->getValue($this->admins) ?: $this->getValue($this->role)); } public function getRules(): array { return [ $this->role => [ 'required' ], $this->admins => [ 'sometimes' ], ]; } }
ProvidesOptions Contract
Queries can provide options that will be appended to the request options
data by implementing the ProvidesOptions
contract. This method should return
a flat array of values that are injected into the response query options data.
<?php declare(strict_types=1); namespace App\Queries; use BayAreaWebPro\SearchableResource\AbstractQuery; use BayAreaWebPro\SearchableResource\Contracts\ProvidesOptions; class ProvidesOptionsQuery extends AbstractQuery implements ProvidesOptions { public string $field = 'role'; protected string $attribute = 'role'; public function __invoke(Builder $builder): void { $builder->where($this->attribute, $this->getValue($this->field)); } public function getOptions(): array { return [ $this->field => [ 'admin', 'editor' ], ]; } }
Options Formatting
Options can be formatted with labels for usage with forms and filters by calling
the labeled()
method on the builder. The labeled method accept a boolean
value which can be used to enable when the request has a session.
You can return preformatted options (label / value array) from queries or use the formatter to generate labeled options.
API Options Schema
Options for API requests are typically not-formatted for speed.
public function getOptions(): array
{
return [
'role' => [
'admin',
'customer'
]
];
}
Blade Options Schema
Options for Blade requests can be formatted for usability.
public function getOptions(): array
{
return [
$this->field => [
[
'label' => 'Admin',
'value' => 'admin'
],
[
'label' => 'Customer',
'value' => 'customer'
]
]
];
}
FormatsOptions Contract
You can override the default formatter by specifying a formatter instance.
SearchableResource::make(User::query())->useFormatter(new OptionsFormatter);
<?php declare(strict_types=1); namespace App\Http\Resources\Formatters; use Illuminate\Support\Collection; use BayAreaWebPro\SearchableResource\OptionsFormatter as Formatter; class OptionsFormatter extends Formatter { /** * @param string $key * @param Collection $options * @return Collection */ public function __invoke(string $key, Collection $options): Collection { if($key === 'abilities'){ return $this->nullable($this->literal($options)); } if($key === 'role'){ return $this->nullable($this->titleCase($options)); } return $this->baseOptions($key, $options); } }
Setting Up Default Options
You can setup a resolving callback in a service provider to pre-bind options to every instance.
use BayAreaWebPro\SearchableResource\OptionsFormatter; use BayAreaWebPro\SearchableResource\SearchableBuilder; $this->app->resolving( SearchableBuilder::class, function (SearchableBuilder $builder){ return $builder ->useFormatter(new OptionsFormatter) ->labeled(request()->hasSession()) ->orderBy('created_at') ->paginate(8) ->sort('desc') ; });
Adding Queries:
Queries can be added two ways. First by referencing the class string for easy bulk usage.
use App\Queries\RoleQuery; SearchableResource::make(User::query()) ->queries([ RoleQuery::class ]);
Second by instantiating each query using the make
method. This can be useful when you need
more methods and logic to determine usage.
use App\Queries\RoleQuery; SearchableResource::make(User::query()) ->query(RoleQuery::make());
Appendable Data
Attributes and fields can be appended to the response by using the following methods:
For model attributes:
SearchableResource::make(User::query()) ->appendable([ 'created_for_humans', 'updated_for_humans', 'bytes_for_humans', ]);
For additional data (appended to the response):
SearchableResource::make(User::query()) ->with([ 'my_key' => [] ]);
{ "my_key": [], "data": [] }
For request fields (appended to the query in response):
SearchableResource::make(User::query()) ->fields([ 'my_filter_state' ]);
{ "query": { "my_filter_state": true } }
When Condition Callback
You can use a callback or invokable class for more control with less method chaining.
class SessionEnabledQuery{ public function __invoke(SearchableBuilder $builder): void { $builder->labeled(); } } SearchableResource::make(User::query()) ->when(request()->hasSession(), new SessionEnabledQuery) ->when(request()->hasSession(), function(SearchableBuilder $builder){ $builder->labeled(); }) ;
Tap Callback
Useful for configuring the builder via an invokable class.
use BayAreaWebPro\SearchableResource\SearchableBuilder; use BayAreaWebPro\SearchableResource\Contracts\InvokableBuilder; class UserSearchable implements InvokableBuilder{ public function __invoke(SearchableBuilder $builder): void { $builder->queries([ RoleQuery::class ]); } } SearchableResource::make(User::query())->tap(new UserSearchable);
Response Output
The relevant query parameters and request options are appended to the output for
convenience. Two additional properties have been added to the pagination parameters
to remove the need for conditionals on the client / user side isFirstPage
and isLastPage
making pagination buttons easy to disable via props (Vue | React).
Note: If the
pagination
method is not used, all pagination related properties will be filtered from the output data.
"data": [
//
],
"pagination": {
"isFirstPage": true,
"isLastPage": true,
...default pagination props...
},
"query": {
"page": 1,
"sort": "desc",
"order_by": "id",
"search": "term",
"per_page": 4,
},
"options": {
"orderable": [
"id",
"name"
],
"sort": [
"asc"
"desc"
]
}
Testing
composer test
composer lint