Skip to content

Commit

Permalink
Merge pull request #58 from krzysztof-gzocha/milestone-3.0
Browse files Browse the repository at this point in the history
Milestone 3.0
  • Loading branch information
krzysztof-gzocha committed May 19, 2016
2 parents 7a35bc4 + 053735e commit 7eff79c
Show file tree
Hide file tree
Showing 88 changed files with 2,731 additions and 964 deletions.
10 changes: 10 additions & 0 deletions .gitattributes
@@ -0,0 +1,10 @@
* text=auto

/bin export-ignore
/docs export-ignore
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/README.md export-ignore
4 changes: 2 additions & 2 deletions .travis.yml
Expand Up @@ -24,8 +24,8 @@ cache:
- $HOME/.composer/cache

before_script:
- if [ -z "$deps" ]; then composer install --ignore-platform-reqs --dev --prefer-source --no-interaction; fi;
- if [ "$deps" == "low" ]; then composer update --ignore-platform-reqs --prefer-lowest --prefer-stable --no-interaction; fi;
- if [ -z "$deps" ]; then composer install --ignore-platform-reqs --dev --prefer-dist --no-interaction; fi;
- if [ "$deps" == "low" ]; then composer update --ignore-platform-reqs --prefer-lowest --prefer-dist --prefer-stable --no-interaction; fi;

script:
- composer coverage
Expand Down
138 changes: 77 additions & 61 deletions README.md
@@ -1,81 +1,88 @@
# Searcher [![Build Status](https://travis-ci.org/krzysztof-gzocha/searcher.svg?branch=master)](https://travis-ci.org/krzysztof-gzocha/searcher) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/?branch=master)
<img align="right" src="https://camo.githubusercontent.com/03659f3fcddeaec49aa2f494c1d4aff0ec9cbd36/687474703a2f2f7777772e636c6b65722e636f6d2f636c6970617274732f612f632f612f382f31313934393936353638313938333637303238396b63616368656772696e642e7376672e7468756d622e706e67"/>

# Searcher [![Build Status](https://travis-ci.org/krzysztof-gzocha/searcher.svg?branch=master)](https://travis-ci.org/krzysztof-gzocha/searcher) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/krzysztof-gzocha/searcher/?branch=master) [![Latest Stable Version](https://poser.pugx.org/krzysztof-gzocha/searcher/v/stable)](https://packagist.org/packages/krzysztof-gzocha/searcher)

### What is that?
*Searcher* is a library created in order to simplify construction of complex searching queries basing on passed models.
*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.4, 7 and HHVM.

### Why?
Did you ever seen code responsible for searching some entities basing on many different criteria? It can be quite a mess!
Did you ever seen code responsible for searching for something basing on many different criteria? It can be quite a mess!
Imagine that you have a form with 20 fields and all of them have their impact on searching conditions.
It's maybe not a great idea to pass whole form to some service at let it parse everything in one place.
Thanks to this library you can split the responsibility of imposing conditions to several smaller classes. One class per model (field). In this way in one `FilterImposer` you only care for one `FilterModel`, which makes it a lot more readable.
You can later use exactly the same `FilterModel` for different search, with different `FilterImposer` and different `SearchingContext` which can use different database.
It's maybe not a great idea to pass whole form to some service at let it parse everything in one place.
Thanks to this library you can split the responsibility of building query criteria to several smaller classes. One class per filter. One `CriteriaBuilder` per `Criteria`.
In this way inside `CriteriaBuilder` you care only for one `Criteria`, which makes it a lot more readable.
You can later use exactly the same `Criteria` for different search, with different `CriteriaBuilder` and even different `SearchingContext` which can use even different database.
You can even use searcher to find **files** on your system thanks to `FinderSearchingContext`.

### Installation
You can install the library via composer by typing:
```
composer require krzysztof-gzocha/searcher
You can install the library via composer by typing in terminal:
```bash
$ composer require krzysztof-gzocha/searcher
```

### Integration
Integration with Symfony is done in **[SearcherBundle](https://github.com/krzysztof-gzocha/searcher-bundle)**

### Idea
- `FilterImposer` - will *impose* new conditions for single model
- `FilterModel` - model that will be passed to `FilterImposer`. It has to be populated from (for example) user input
- `SearchingContext` - context of single search. This service should know how to fetch results from constructed query and it holds something called `QueryBuilder`, but it can be anything that works for you. This is an abstraction layer between search and database. There is different context for Doctrine's ORM, ODM, Elastica and so on,
- `Searcher` - holds collection of `FilterImposer` and will pass `FilterModel` to apropriate `FilterImposer`.
- `CriteriaBuilder` - will build new *conditions* for single `Criteria`,
- `Criteria` - model that will be passed to `CriteriaBuilder`. You just need to hydrate it somehow, so it will be useful. Criteria can hold multiple fields inside and all (or some) of them might be used inside `CriteriaBuilder`,
- `SearchingContext` - context of single search. This service should know how to fetch results from constructed query and it holds something called `QueryBuilder`, but it can be anything that works for you - any service. This is an abstraction layer between search and database. There are different contexts for Doctrine's ORM, ODM, Elastica, *Files* and so on. If there is no context for you you can implement one - it's shouldn't be hard,
- `Searcher` - holds collection of `CriteriaBuilder` and will pass `Criteria` to appropriate `CriteriaBuilder`.

### Example
Let's say we want to search for **people** whose **age** is in some filterd range.
In this example we will use Doctrine's QueryBuilder, so we will use `QueryBuilderSearchingContext` and will specify in `FilterImposer` that it should interact only with `Doctrine\ORM\QueryBuilder`, but we do **not** have to use only Doctrine.
Let's say we want to search for **people** whose **age** is in some filtered range.
In this example we will use Doctrine's QueryBuilder, so we will use `QueryBuilderSearchingContext` and will specify in our `CriteriaBuidler` that it should interact only with `Doctrine\ORM\QueryBuilder`, but remember that we do **not** have to use only Doctrine.

First of all we would need to create `AgeRangeFilterModel` - the class that will holds values of minimal and maximal age. There are already implemented default FilterModels in [here](https://github.com/krzysztof-gzocha/searcher/tree/master/src/KGzocha/Searcher/FilterModel).
#### 1. Criteria
First of all we would need to create `AgeRangeCriteria` - the class that will holds values of minimal and maximal age. There are already implemented default `Criteria` in [here](https://github.com/krzysztof-gzocha/searcher/tree/master/src/KGzocha/Searcher/Criteria).
```php
class AgeRangeFilterModel implements FilterModelInterface
class AgeRangeCriteria implements CriteriaInterface
{
private $minimalAge;
private $maximalAge;

/**
* Only required method.
* If will return true, then it will be passed to some of the FilterImposer(s)
* If will return true, then it will be passed to some of the CriteriaBuilder(s)
*/
public function isImposed()
public function shouldBeApplied()
{
return null !== $this->minimalAge && null !== $this->maximalAge;
}

// getters, setters, what ever
// getters, setters, whatever
}
```

#### 2. CriteriaBuilder
In second step we would like to specify conditions that should be imposed for this model.
That's why we would need to create `AgeRangeFilterImposer`
That's why we would need to create `AgeRangeCriteriaBuilder`
```php
class AgeRangeFilterImposer implements FilterImposerInterface
class AgeRangeCriteriaBuilder implements CriteriaBuilderInterface
{
public function imposeFilter(
FilterModelInterface $filterModel,
public function buildCriteria(
CriteriaInterface $criteria,
SearchingContextInterface $searchingContext
) {
$searchingContext
->getQueryBuilder()
->andWhere('e.age >= :minimalAge')
->andWhere('e.age <= :maximalAge')
->setParameter('minimalAge', $filterModel->getMinimalAge())
->setParameter('maximalAge', $filterModel->getMaximalAge());
->setParameter('minimalAge', $criteria->getMinimalAge())
->setParameter('maximalAge', $criteria->getMaximalAge());
}

public function supportsModel(
FilterModelInterface $filterModel
public function allowsCriteria(
CriteriaInterface $criteria
) {
// No need to check isImposed(). Searcher will check it
return $filterModel instanceof AgeRangeFilterModel;
return $criteria instanceof AgeRangeCriteria;
}

/**
* You can skip this method if you will extend from QueryBuilderFilterImposer.
* You can skip this method if you will extend from AbstractORMCriteriaBuilder.
*/
public function supportsSearchingContext(
SearchingContextInterface $searchingContext
Expand All @@ -84,73 +91,77 @@ class AgeRangeFilterImposer implements FilterImposerInterface
}
}
```
In next steps we would need to create collections for both: models and imposers.
#### 3. Collections
In next steps we would need to create collections for both: `Criteria` and `CriteriaBuidler`.
```php
$imposers = new FilterImposerCollection();
$builders = new CriteriaBuilderCollection();

$imposers->addFilterImposer(new AgeRangeFilterImposer());
$imposers->addFilterImposer(/** rest of filter imposers */);
$builders->addCriteriaBuilder(new AgeRangeCriteriaBuilder());
$builders->addCriteriaBuilder(/** rest of builders */);
```
```php
$ageRangeModel = new AgeRangeModel();
$ageRangeCriteria = new AgeRangeCriteria();

// We have to populate the model before searching
$ageRangeModel->setMinimalAge(23);
$ageRangeModel->setMaximalAge(29);
$ageRangeCriteria->setMinimalAge(23);
$ageRangeCriteria->setMaximalAge(29);

$models = new FilterModelCollection();
$models->addFilterModel($ageRangeModel);
$models->addFilterModel(/** rest of models */);
$criteria = new CriteriaCollection();
$criteria->addCriteria($ageRangeCriteria);
$criteria->addCriteria(/** rest of criteria */);
```

#### 4. SearchingContext
Now we would like to create our `SearchingContext` and populate it with QueryBuilder taken from Doctrine ORM.
```php
$context = new QueryBuilderSearchingContext($queryBuilder);

$searcher = new Searcher($imposers, $context);
$searcher->results($models); // Yay, we have our results!
$searcher = new Searcher($builders, $context);
$searcher->search($criteriaCollection); // Yay, we have our results!
```

If there is even small chance that your QueryBuilder will return `null` when you are expecting traversable object or array then you can use `WrappedResultsSearcher` instead of normal `Searcher` class. It will act exactly the same as `Searcher`, but it will return `ResultCollection`, which will work only with array or `\Traversable` and if result will be just `null` your code will still work. Here is how it will looks like:
```php
$searcher = new WrappedResultsSearcher(new Searcher($imposers, $context));
$results = $searcher->results($model); // instance of ResultCollection
$searcher = new WrappedResultsSearcher(new Searcher($builders, $context));
$results = $searcher->search($criteriaCollection); // instance of ResultCollection
foreach ($results as $result) {
// will work!
}

foreach ($results->getResults() as $result) {
// Since ResultCollection has method getResults(0 this will also work!
// Since ResultCollection has method getResults() this will also work!
}
```
### Order
In order to sort your results you can make use of already implemented FilterModel. You don't need to implement it from scratch. Keep in mind that you still need to implement your FilterImposer for it (this feature is still under development). Let's say you want to order your results and you need value `p.id` in your FilterImposer to do it, but you would like to show it as `pid` to end-user. Nothing simpler!
This is how you can create FilterModel:
In order to sort your results you can make use of already implemented `Criteria`. You don't need to implement it from scratch. Keep in mind that you still need to implement your `CriteriaBuilder` for it (this feature is still under development). Let's say you want to order your results and you need value `p.id` in your CriteriaBuidler to do it, but you would like to show it as `pid` to end-user. Nothing simpler!
This is how you can create OrderByCriteria:
```php
$mappedFields = ['pid' => 'p.id', 'valueForUser' => 'valueForImposer'];
$model = new MappedOrderByAdapter(
new OrderByFilterModel('pid'),
$mappedFields = ['pid' => 'p.id', 'valueForUser' => 'valueForBuilder'];
$criteria = new MappedOrderByAdapter(
new OrderByCriteria('pid'),
$mappedFields
);
// $model->getMappedOrderBy() = 'p.id'
// $model->getOrderBy() = 'pid'
// $criteria->getMappedOrderBy() = 'p.id'
// $criteria->getOrderBy() = 'pid'
```
Of course you don't need to use `MappedOrderByAdapter` - you can use just `OrderByFilterModel`, but then user will know exactly what fields are beeing used to sort.
Of course you don't need to use `MappedOrderByAdapter` - you can use just `OrderByCriteria`, but then user will know exactly what fields are beeing used to sort.
### Pagination
FilterModel for pagination is also implemented and you don't need to do it, but keep in mind that you still need to implement FilterImposer that will make use of this FilterModel and do actual pagination (this feature is under development).
`Criteria` for pagination is also implemented and you don't need to do it, but keep in mind that you still need to implement `CriteriaBuilder` that will make use of it and do actual pagination (this feature is under development).
Let's say you want to allow your end-user to change pages, but not number of items per page.
You can use this example code:
```php
$model = new ImmutablePaginationAdapter(
new PaginationFilterModel($page = 1, $itemsPerPage = 50)
$criteria = new ImmutablePaginationAdapter(
new PaginationCriteria($page = 1, $itemsPerPage = 50)
);
// $model->setItemsPerPage(250); <- use can try to change it
// $model->getItemsPerPage() = 50 <- but he can't actualy do it
// $model->getPage() = 1
// $criteria->setItemsPerPage(250); <- use can try to change it
// $criteria->getItemsPerPage() = 50 <- but he can't actualy do it
// $criteria->getPage() = 1
```
Of course if you want to allow user to change number of items per page also you can skip the `ImmutablePaginationAdapter` and use just `PaginationFilterModel`.
Of course if you want to allow user to change number of items per page also you can skip the `ImmutablePaginationAdapter` and use just `PaginationCriteria`.
### Contributing
All ideas and pull requests are welcomed and appreciated :)
If you have any problem with usage don't hesitate to create an issue, we can figure your problem out together.

### Development
Command to run test: `composer test`
Expand All @@ -161,3 +172,8 @@ In alphabetical order
- https://github.com/pawelhertman
- https://github.com/ustrugany
- https://github.com/wojciech-olszewski


#### License
License: MIT
Author: Krzysztof Gzocha [![](https://img.shields.io/badge/Twitter-%40kgzocha-blue.svg)](https://twitter.com/kgzocha)
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -36,7 +36,8 @@
"phpmd/phpmd": "^2.4.3",
"doctrine/orm": "^2.5.0",
"doctrine/mongodb-odm": "^1.0.0",
"ruflin/elastica": ">=2.0.0"
"ruflin/elastica": ">=2.0.0",
"symfony/finder": "^2.0.5"
},
"scripts": {
"test": ["phpunit tests/"],
Expand Down

0 comments on commit 7eff79c

Please sign in to comment.