krzysztof-gzocha / searcher-bundle
Bridge between searcher and Symfony2 framework
Requires
- php: >=5.5.9
- krzysztof-gzocha/searcher: ~4.0.0
- symfony/framework-bundle: >=2.3
Requires (Dev)
- knplabs/knp-components: >=1.2.4
- knplabs/knp-paginator-bundle: ^2.2.0
- phpdocumentor/reflection-docblock: 2.0.4
- phpunit/phpunit: ^4.8
Suggests
- doctrine/mongodb-odm: to use with Doctrine's ODM
- doctrine/orm: to use with Doctrine's ORM
- knplabs/knp-paginator-bundle: to use with Knp Paginator bundle
- symfony/form: to use with Symfony Form component
README
SearcherBundle
This bundle is providing integration between Symfony and Searcher
What is Searcher?
Searcher is a library completely decoupled from any framework created in order to simplify construction of complex searching queries basing on passed criteria. It's basic idea is to split each searching filter to separate class. Regardless of what do you want to search: entities in MySQL, MongoDB or just files. Supported PHP versions: >=5.5, 7 and HHVM.
Full documentation
Full documentation can be found at http://searcher.rtfd.io/
Installation
You can install this bundle via composer
composer require krzysztof-gzocha/searcher-bundle
and don't forget to register it in your AppKernel:
public function registerBundles() { $bundles = array( /** Your bundles **/ new KGzocha\Bundle\SearcherBundle\KGzochaSearcherBundle(), ); /** rest of the code **/ }
Example usage
Config
In config file we will specify minimal configuration for people
context.
You can full full example of config reference in here
k_gzocha_searcher: contexts: people: context: service: your_searching_context_service_id criteria: - { class: \AgeRangeCriteria, name: age_range} - { service: some_service_id, name: other_criteria } builders: - { class: \AgeRangeCriteriaBuilder, name: age_range } - { service: other_service_id, name: my_criteria_builder }
As you can see you can specify everything as a simple class or as your own service.
This configuration will create our people
context and create all required services (builder collection, criteria collection, searcher and context), so you can access them and make use of them. For example to access Searcher instance from controller you can simply:
$this->get('k_gzocha_searcher.people.searcher');
or to access age_range
criteria:
$this->get('k_gzocha_searcher.people.criteria.age_range');
or access my_criteria_builder
:
$this->get('k_gzocha_searcher.people.builder.my_criteria_builder');
I guess it's pretty easy to understand this naming convention.
In this example you need to define only 1 service on your own - SearchingContext service with id specified in the config (your_searching_context_service_id
). You can do it like this:
your_searching_context_service_id: class: KGzocha\Searcher\Context\QueryBuilderSearchingContext arguments: - @my_search.query_builder # Or any QueryBuilder service
Code
For this example we will use simple AgeRangeCriteria (described in here), but of course you can use your own class or service.
class AgeRangeCriteria implements CriteriaInterface { private $minimalAge; private $maximalAge; /** * Only required method. */ public function shouldBeApplied() { return null !== $this->minimalAge && null !== $this->maximalAge; } // getters, setters, what ever }
We will also use AgeRangeCriteriaBuilder (described in here), but of course you can use your own class or service.
class AgeRangeCriteriaBuilder implements FilterImposerInterface { public function buildCriteria( CriteriaInterface $criteria, SearchingContextInterface $searchingContext ) { $searchingContext ->getQueryBuilder() ->andWhere('e.age >= :minimalAge') ->andWhere('e.age <= :maximalAge') ->setParameter('minimalAge', $criteria->getMinimalAge()) ->setParameter('maximalAge', $criteria->getMaximalAge()); } public function allowsCriteria( CriteriaInterface $criteria ) { // No need to check shouldBeApplied(). Searcher will check it return $criteria instanceof AgeRangeCriteria; } /** * You can skip this method if you will extend from QueryBuilderFilterImposer. */ public function supportsSearchingContext( SearchingContextInterface $searchingContext ) { return $searchingContext instanceof \KGzocha\Searcher\Context\Doctrine\QueryBuilderSearchingContext; } }
Form (optional)
Now we can create example form. Form will allow Symfony to take care of population and validation our criteria from request. This step is optional and you don't have to populate criteria from request. You can do this however you want to.
use KGzocha\Bundle\SearcherBundle\Form\SearchForm; class MySearchForm extends SearchForm { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('minimalAge', 'integer', [ 'property_path' => $this->getPath('ageRange', 'minimalAge'), ]) ->add('maximalAge', 'integer', [ 'property_path' => $this->getPath('ageRange', 'maximalAge'), ]) /** and any other fields.. **/ ->add('<PARAM NAME IN REQUEST>', '<ANY FORM TYPE>', [ 'property_path' => $this->getPath( '<CRITERIA NAME FROM CONFIG>', '<CRITERIA ATTRIBUTE NAME>' ), ]); } }
Controller
public function searchAction(Request $request) { $form = $this->createForm( new MySearchForm(), $this->get('k_gzocha_searcher.people.criteria_collection') ); $form->handleRequest($request); // Now we can check if form is valid $searcher = $this->get('k_gzocha_searcher.people.searcher'); $results = $searcher->search($form->getData()); // Yay, we have our results! // $results is instance of ResultCollection by default. Read for 'wrapper_class' }
Wrapper class
By default SearcherBundle will wrap Searcher into WrappedResultsSearcher
,
which will return ResultCollection
which has method getResults()
that will return collection of your results.
Of course ResultCollection
itself is traversable, so you can use it inside foreach
loop.
This feature is useful in rare situations where you are not sure if your QueryBuilder
will return array or traversable object. Returning null
and trying to iterate over it will lead to an error. ResultCollection will prevent this kind of situation. If you want to change wrapper class then you need to specify wrapper_class
in searcher config.
Of course sometimes you want your Searcher to just return an integer or whatever, then you do not want to wrap your Searcher. In order to do that just specify wrapper_class
as null
Chain searching
Searcher library allows you to perform chain searching and
you can use with this bundle as well. All what you need to do is to properly configure it in config file and fetch
ChainSearch
service.
Example chain searching config:
k_gzocha_searcher: chains: people_log: # optional chain_searcher: class: \KGzocha\Searcher\Chain\ChainSearch service: chain_searcher_service transformers: - name: peopleIdToLogId service: transfomer_service class: \TransformerClass # at least two are required cells: - name: peopleCell searcher: people transformer: peopleIdToLogId class: \KGzocha\Searcher\Chain\Cell # optional service: cell_service_1 # optional - name: logCell searcher: logs transformer: ~ # If empty EndTransformer will be used class: \KGzocha\Searcher\Chain\Cell # optional service: cell_service_2 # optional
With above config you can easily fetch all services like this:
$this->get('k_gzocha_searcher.chains.people_log.searcher'); // ChainSearch service $this->get('k_gzocha_searcher.chains.people_log.cell.peopleCell'); // #1 Cell service $this->get('k_gzocha_searcher.chains.people_log.cell.logCell'); // #2 Cell service $this->get('k_gzocha_searcher.chains.people_log.transformer.peopleToLogId'); // Transformer service
Contributing
All ideas and pull request are welcomed and appreciated. Please, feel free to share your thought via issues.
Command to run tests: composer test
.