bayareawebpro/searchable-resource

This package is abandoned and no longer maintained. No replacement package was suggested.

Searchable Resource Builder for Laravel

v1.0.29 2022-02-09 00:21 UTC

README

CI Coverage Downloads Version MIT

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