ruflin/Elastica — pull request #724 by webdevsHub (2014-11-16 14:14:26+00:00)
Title: QueryBuilder

Hi, after the ScanAndScoll-Iterator I wrote a QueryBuilder for Elastica!

# Goals

* full DSL support
* version check (is that command available in my elasticsearch installation?)
* convenience (autocomplete, links to doc, good exception messages)
* extendable (add/overwrite parts of the DSL)

# Implementation

I took the idea from [predis](https://github.com/nrk/predis), because they have a similar situation:

1. Every redis command is represented by a class in predis (like in Elastica: every Query/Filter/... has a class).
2. They have Profile-Classes (I called them Version) which contains all supported commands of a Version.
3. To execute a command via Client-Class predis uses __call() magic function to check whether or not that command is supported in configured version.

My implementation is different in some ways:

## DSL part classes
The existent command classes in Elastica can be used for QueryBuilder because they have a fluent interface. So I only had to create wrapper classes to easily access/construct these classes.

I created one class for each elasticsearch DSL part (Query, Filter, Aggregation, Suggesters) which constructs every existent Elastica-Object with a convenient method definition. You can find this classes in `Elastica\QueryBuilder\DSL`.

This is the heart of QueryBuilder and all other stuff is optional. If you don't want the Version-check you could just use this classes:

```
$queryDSL = new \Elastica\QueryBuilder\Query();
$queryDSL->match('field', 'value'); // returns Elastica\Query\Match
```

I added every available method according to elasticsearch documentation to the DSL classes. In case a method is not implemented in Elastica (e.g. query "custom_filters_score") you get a `Elastica\Exception\NotImplementedException`.

Most of the DSL methods just pipe the required Constructor arguments to the class, but I changed some method definitions for convenience reasons (e.g. `Elastica\QueryBuilder\DSL\Query::match()`)

There is only one single Filter, that causes problems: `Elastica\QueryBuilder\DSL\Filter::geo_shape()` 2 implementations exists: GeoShapeProvided, GeoShapePreIndexed. Maybe you have an idea to solve this?!

## QueryBuilder
`Elastica\QueryBuilder` takes a `Elastica\QueryBuilder\Version` object and registers the default DSL objects. You can access this DSL object via `__call()` (names defined by `Elastica\QueryBuilder\DSL::getType()`) or via `$qb->query()` `$qb->filter()` `$qb->agg()` `$qb->suggest()` convenience methods.

Add or overwrite a DSL object with `Elastica\QueryBuilder::addDSL()`

### Facade
Version check is done by `Elastica\QueryBuilder\Facade`. It wraps a DSL with a Version object and acts like a whitelist firewall. Note that the QueryBuilder convenience methods have a "wrong" @return annotation to get correct autocomplete.

The exception messages in Facade are very important, because they provide exact information about what is not working. For this reason I wrote tests for the exception messages.

## Tests
I'm not sure if we should test DSL and Version classes, because they are basicly configurations. The actual work is done by the returned objects and they are tested pretty well. I would like to discuss this!

I added a showcase for QueryBuider usage in `Elastica/Test/QueryBuilderTest::testQueryBuilder()`.

# Conclusion
I'm open to critics, even if you want major changes. Lets discuss the QueryBuilder topic without any restrictions, because I think a QueryBuilder is a very important functionallity for a Client library. We can just merge my current implementation or see it as a first proposal and continue working on it.