tobento / app-search
App search support.
Requires
- php: >=8.0
- tobento/app: ^1.0.7
- tobento/app-http: ^1.0
- tobento/app-language: ^1.0
- tobento/app-migration: ^1.0
- tobento/app-translation: ^1.0
- tobento/app-view: ^1.0
- tobento/css-modal: ^1.0
- tobento/service-collection: ^1.0
- tobento/service-menu: ^1.0
- tobento/service-pagination: ^1.0
- tobento/service-repository: ^1.0
- tobento/service-support: ^1.0
- tobento/service-view: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- tobento/app-database: ^1.0
- tobento/app-testing: ^1.0
- tobento/service-container: ^1.0
- tobento/service-repository-storage: ^1.0
- vimeo/psalm: ^4.0
README
The search app provides interfaces to search resources by filters. It comes with a default implementation and some basic searchables and filters.
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app search project running this command.
composer require tobento/app-search
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Search Boot
The search boot does the following:
- migrates search config, view and asset files
- implements search interfaces based on the search config file
- boots features defined in config file
use Tobento\App\AppFactory; use Tobento\App\Search\InputInterface; use Tobento\App\Search\SearchInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Search\Boot\Search::class); $app->booting(); // Implemented interfaces: $search = $app->get(SearchInterface::class); $input = $app->get(InputInterface::class); // Run the app $app->run();
Search Config
The configuration for the search is located in the app/config/search.php
file at the default App Skeleton config location where you can configure the implemented search interfaces for your application and more.
Basic Usage
Searching
This example shows how a search process works in general. If you use the Search Feature, there is no need to implement anything.
use Tobento\App\Search\Filters; use Tobento\App\Search\FiltersInterface; use Tobento\App\Search\Input; use Tobento\App\Search\InputInterface; use Tobento\App\Search\Search; use Tobento\App\Search\Searchables; use Tobento\App\Search\SearchablesInterface; use Tobento\App\Search\SearchInterface; use Tobento\App\Search\SearchResultsInterface; $search = new Search( filters: new Filters(), // FiltersInterface searchables: new Searchables(), // SearchablesInterface ); var_dump($search instanceof SearchInterface); // bool(true) // Searching: $searchResults = $search->search( input: new Input(['search' => ['term' => 'foo']]), // InputInterface ); var_dump($searchResults instanceof SearchResultsInterface); // bool(true) // You may get the filters: var_dump($search->filters() instanceof FiltersInterface); // bool(true) // You may get the searchables: var_dump($search->searchables() instanceof SearchablesInterface); // bool(true)
Features
Search Feature
The search feature provides a simple search page where users can search the applications content. Furthermore, it provides a searchbar which you may render on all pages.
Check out the Search section on how to add searchables, filters and more as this feature uses the implemented search interfaces.
Config
In the config file you can configure this feature:
'features' => [ new Feature\Search( // The default views to render: view: 'search/index', viewSearchbar: 'search/searchbar', viewSearchbarResults: 'search/searchbar-results', // If true, routes are being localized. localizeRoute: false, ), ],
Search Page
The search page is available under the https://example.com/search
url.
Searchbar
In any view file, render the view named search.bar
which will display a search input element:
<header><?= $view->render('search.bar') ?></header>
Search
Adding Searchables
You may use and add the Available Searchables or create your own searchables if desired.
Option 1
In the Search Config file you can configure your searchables:
use Psr\Container\ContainerInterface; use Tobento\App\Search\Search; use Tobento\App\Search\Searchable; use Tobento\App\Search\Searchables; use Tobento\App\Search\SearchInterface; use Tobento\App\Search\Filter; use Tobento\App\Search\Filters; 'interfaces' => [ SearchInterface::class => static function(ContainerInterface $container): SearchInterface { return new Search( filters: new Filters( new Filter\SearchTerm(name: 'search.term'), ), searchables: new Searchables( new Searchable\Menus($container->get(\Tobento\Service\Menu\MenusInterface::class)->menu('main')), ), ); }, ],
Option 2
Use the App on method to add searchables only on demand:
use Tobento\App\Search\Searchable; use Tobento\App\Search\SearchablesInterface; use Tobento\App\Search\SearchInterface; $app->on( SearchInterface::class, static function(SearchInterface $search) use ($app): void { // Get the searchables: $searchables = $search->searchables(); // SearchablesInterface // Adding a searchable: $menu = $app->get(\Tobento\Service\Menu\MenusInterface::class)->menu('main'); $searchables->add(searchable: new Searchable\Menus(menu: $menu)); } );
Creating Searchables
You may create searchables for specific resources if you need full search and filter control.
Inspect the following files to see different searchable implementations:
Tobento\App\Search\Searchable\Menu::class
Tobento\App\Search\Searchable\Repository::class
Example
use Tobento\App\Search\Filter; use Tobento\App\Search\FilterInterface; use Tobento\App\Search\FiltersInterface; use Tobento\App\Search\SearchableInterface; use Tobento\App\Search\SearchResult; use Tobento\App\Search\SearchResultInterface; use Tobento\Service\Pagination\Pagination; use Tobento\Service\Pagination\PaginationInterface; use Tobento\Service\Pagination\UrlGenerator; use Tobento\Service\View\ViewInterface; class ProductsSearchable implements SearchableInterface { protected null|PaginationInterface $pagination = null; public function __construct( protected ProductRepository $repository, protected ViewInterface $view, protected int $priority = 0, ) {} public function name(): string { return 'products'; } public function title(): string { return 'Products'; } public function priority(): int { return $this->priority; } /** * Returns the filters. * * @return array<array-key, FilterInterface> */ public function filters(): array { // Configure specific products filters defining the searchable attribute on each filter: return [ new Filter\Select( name: 'search.product-color', label: 'Colors', searchable: $this->name(), options: ['red' => 'Red', 'blue' => 'Blue'], ), new Filter\Pagination( name: 'search.product-page', searchable: $this->name(), pagination: $this->pagination(), ), ]; } /** * Returns the search results found. * * @param FiltersInterface $filters * @return array<array-key, SearchResultInterface> */ public function search(FiltersInterface $filters): array { $where = []; $orderBy = []; $limit = []; // Use the filters you wish to apply to your query: foreach($filters as $filter) { switch ($filter::class) { case Filter\Select::class: if ($filter->name() === 'search.product-color') { // apply filter to query: } break; case Filter\SearchTerm::class: // apply filter to query: break; case Filter\Pagination::class: if ($filter->name() === 'search.product-page') { $this->pagination = $filter->pagination(); $limit = [$this->pagination->getItemsPerPage(), $this->pagination->getItemsOffset()]; } break; } } if ($filters->has('search.product-page')) { $paginationFilter = $filters->get('search.product-page'); $paginationFilter->updatePaginationTotalItems($this->repository->count(where: $where)); $this->pagination = $paginationFilter->pagination(); $limit = [$this->pagination->getItemsPerPage(), $this->pagination->getItemsOffset()]; } // Do the query and create the search results: $results = []; foreach($this->repository->findAll(where: $where, orderBy: $orderBy, limit: $limit) as $item) { $results = new SearchResult( searchable: $this->name(), type: $this->title(), title: $item->get('title'), url: $item->get('url'), image: $item->get('image'), // you may add html for custom search result: html: $this->view->render( view: 'product/search-result', data: $item, ), ); } return $results; } public function totalItems(): int { return $this->pagination()->getTotalItems(); } public function pagination(): PaginationInterface { if ($this->pagination) { return $this->pagination; } return $this->pagination = new Pagination( totalItems: $this->repository->count(), currentPage: 1, itemsPerPage: 25, maxPagesToShow: 6, maxItemsPerPage: 100, urlGenerator: (new UrlGenerator())->addPageUrl('?search[product-page]={num}'), ); } }
Adding Filters
Option 1
In the Search Config file you can configure your filters:
use Psr\Container\ContainerInterface; use Tobento\App\Search\Search; use Tobento\App\Search\Searchable; use Tobento\App\Search\Searchables; use Tobento\App\Search\SearchInterface; use Tobento\App\Search\Filter; use Tobento\App\Search\Filters; 'interfaces' => [ SearchInterface::class => static function(ContainerInterface $container): SearchInterface { return new Search( filters: new Filters( new Filter\SearchTerm(name: 'search.term'), ), searchables: new Searchables( new Searchable\Menus($container->get(\Tobento\Service\Menu\MenusInterface::class)->menu('main')), ), ); }, ],
Option 2
Use the App on method to add filters only on demand:
use Tobento\App\Search\Filter; use Tobento\App\Search\FiltersInterface; use Tobento\App\Search\SearchInterface; $app->on( SearchInterface::class, static function(SearchInterface $search) use ($app): void { // Get the filters: $filters = $search->filters(); // FiltersInterface // Adding a filter: $filters->add(filter: new Filter\SearchTerm(name: 'search.term')); } );
Creating Filters
You may create custom filters to fit your requirements.
Inspect the following files to see different filter implementations:
Tobento\App\Search\Filter\Clear::class
Tobento\App\Search\Filter\Searchables::class
Tobento\App\Search\Filter\SearchTerm::class
Available Searchables
Menu Searchable
The Menu
searchable allows you to search the provided menu items using the Search Term Filter.
use Tobento\App\Search\Searchable; use Tobento\Service\Menu\MenuInterface; $searchable = new Searchable\Menu( menu: $menu, // MenuInterface // Set a unique name: name: 'menu', // You may set a custom title: title: 'Menu Items', // You may set a priority: priority: 100, );
You may check out the Menu Service for more information.
Repository Searchable
The Repository
searchable allows you to search the provided repository using the Search Term Filter.
use Tobento\App\Search\Searchable; use Tobento\App\Search\SearchResult; use Tobento\App\Search\SearchResultInterface; use Tobento\Service\Repository\RepositoryInterface; $searchable = new Searchable\Repository( repository: $repository, // RepositoryInterface // Set a unique name: name: 'products', // Set a title: title: 'Products', // Define the search attributes: searchAttributes: ['title', 'description'], // Map the repository items to the search results: toSearchResult: function(object $item, Searchable\Repository $searchable): SearchResultInterface { return new SearchResult( searchable: $searchable->name(), type: $searchable->title(), title: $item->get('title'), description: $item->get('description'), url: $item->get('url'), image: $item->get('image'), ); }, // You may set a priority: priority: 100, );
You may check out the Repository Service for more information.
Available Filters
Clear Filter
This filter displays a button to clear all filters.
use Tobento\App\Search\Filter; $filter = new Filter\Clear( // Set a unique name: name: 'search.clear', // You may set a custom label: label: 'Clear all', // You may set attributes for the button element: attributes: ['data-foo' => 'value'], );
Input Filter
This filter displays a HTML input element to filter searchables.
use Tobento\App\Search\Filter; $filter = new Filter\Input( // Set a unique name: name: 'search.price.from', // You may set a label: label: 'Price From', // You may set a description: description: 'Searches prices from.', // You may set a searchable the filter belongs to: searchable: 'products', // or null // You may change the default input type (text): inputType: 'range', // You may set attributes for the input element: inputAttributes: ['min' => '5'], // You may set a default input value: inputValue: '10', // or null // You may set a custom view to render: view: 'search/filter', );
Pagination Filter
The main purpose of this filter is to use for the searchables pagination. Check out the Available Searchables class files to see its usage in action.
Searchables Filter
This filter displays checkboxes with the searchables to enable or disable for searching.
use Tobento\App\Search\Filter; $filter = new Filter\Searchables( // Set a unique name: name: 'search.searchables', // You may set a custom label: label: 'Content', // You may set a description: description: 'Choose the contents you wish to search.', // You may set a custom view to render: view: 'search/filter', );
Search Term Filter
This filter displays a search input element to search for the provided search term.
use Tobento\App\Search\Filter; $filter = new Filter\SearchTerm( // Set a unique name: name: 'search.term', // You may set a label: label: 'Search', // You may set a description: description: 'Search for ...', // You may set a custom placeholder text: placeholder: 'Search ...', // You may set a custom view to render: view: 'search/filter', );
Select Filter
This filter displays a HTML select element with options to filter searchables.
use Tobento\App\Search\Filter; $filter = new Filter\Select( // Set a unique name: name: 'search.colors', // You may set a label: label: 'Colors', // You may set a description: description: 'Choose any colors you wish to search for.', // You may set a searchable the filter belongs to: searchable: 'products', // or null // Set the options to choose from: options: ['blue' => 'Blue', 'red' => 'Red'], // with optgroup options: // options: ['Primary' => ['blue' => 'Blue']], // You may set a default value: selected: ['blue'], // You may set attributes for the select element: selectAttributes: ['multiple'], // You may set attributes for the option elements: optionAttributes: ['*' => ['data-foo' => 'val']], // You may set attributes for the optgroup elements: optgroupAttributes: ['data-foo' => 'value'], // You may set a custom view to render: view: 'search/filter', );