Skip to content

Commit

Permalink
doc: aggregation and like filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Dec 27, 2020
1 parent 1c77032 commit a21e4ae
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 79 deletions.
178 changes: 178 additions & 0 deletions doc/collection-filtering.texy
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
Collection Filtering
####################

Collection is filtered through its `findBy()` method; alternatively, shortcut `getBy()` accepts the same filtering expressions.

The simplest filtering is an array of conditions. These conditions are passed as the only parameter of the `findBy()` method. The associative array consists of entity property names and and their wanted values. Keys can contain an optional operator. The default operator is equality. Let's see the example:

/--php
$books = $orm->books->findBy([
'author' => $author->id,
'publishedAt<=' => new DateTimeImmutable(),
]);
\--

Allowed operators are `=`, `!=`, `<=`, `<`, `>=`, `>`, and `~` (like -- see later), append it directly after the property name (without an space or other white-char).

You can filter the collection by conditions with condition filtering by a relationships traversing; use a *traversing expression*: it consists of the path delimited by `->` - the same arrow you use in PHP.

/--php
// find all books which were authored by Jon Snow
$orm->books->findBy(['author->name' => 'Jon Snow']);

// find all books which were not translated by Jon Snow
$orm->books->findBy(['translator->name!=' => 'Jon Snow']);
\--

The described syntax may be expanded to support a `OR` logical conjunction. Prepend the `ICollection::OR` operator as a first value of the filtering array:

/--php
// finds all books which were authored or translated by one specific person
$books = $orm->books->findBy([
ICollection::OR,
'author->name' => 'Jon Snow',
'translator->name' => 'Jon Snow',
]);
\--

This relationship filtering is designed mainly for has-one relationship. Has-many relationship usually utilize an aggregation function, which is covered later in this chapter. Still, this filtering syntax works for has-many relationship. Such expression will select all entries where at least one of the entities in has-many relationship meets the conditions.

You may nest the filtering structure; use the same syntax repeatedly:

/--php
// find all man older than 10 years and woman younger than 12 years
$authors = $orm->author->findBy([
ICollection::OR,
[
ICollection::AND,
'age>=' => 10,
'sex' => 'male',
],
[
ICollection::AND,
'age<=' => 12,
'sex' => 'female',
],
]);
\--

The previous example can be shortened because the `AND` operator is the default logical operator.

/--php
// find all man older than 10 years and woman younger than 12 years
$authors = $orm->author->findBy([
ICollection::OR,
[
'age>=' => 10,
'gender' => 'male',
],
[
'age<=' => 12,
'gender' => 'female',
],
]);
\--

.[note]
Filtering over virtual properties is generally unsupported and provides undefined behavior.

-----------


LIKE filtering
==============

`LIKE` filtering is supported and directly provided in Nextras Orm. Use `~` compare operator. The value has to be wrapped as `Nextras\Orm\Collection\Expression\LikeExpression` instance, use its static builders to create one: choose from `startsWith`, `endsWith` or `contains`. Alternatively, you may provide your wildcard expression with `raw` method. Be aware, that raw method expects sanitized input.

/--php
// finds all users with email hosted on gmail.com
$users->findBy([
'emails~' => LikeExpression::endsWith('@gmail.com'),
]);
\--

----------


Aggregation
===========

Aggregation functions can be used for both collection filtering and sorting. They are based on [collection functions | collection-functions] -- a general approach for custom collection modification.

Orm brings these prepared aggregation functions:

- CountAggregateFunction
- SumAggregateFunction
- AvgAggregateFunction
- MinAggregateFunction
- MaxAggregateFunction

All those functions are implemented both for Dbal and Array collections and they are registered in repository as commonly provided collection functions.

To use a collection function, pass the function name and then its arguments –- all aggregation functions take only one argument – an expression that should be aggregated. Let’s see an example:

/--php
use Nextras\Orm\Collection\Functions\CountAggregateFunction;

$authorsCollection->orderBy(
[CountAggregateFunction::class, 'books->id']
);
\--

In the example we sort the collection of authors by the count of their books, i.e. authors with the least books will be at the beginning. The example allows the same "property expression" you use for filtering. You can reverse the ordering:

/--php
use Nextras\Orm\Collection\Functions\CountAggregateFunction;
use Nextras\Orm\Collection\ICollection;

$authorsCollection->orderBy(
[CountAggregateFunction::class, 'books->id'],
ICollection::DESC
);
\--

Filtering by an aggregation requires a little more. Let's filter the collection by authors who have written more than 2 books. Using `CountAggregationFunction` itself won’t be enough. You need to compare its result with the wanted number, `2` this time. To do so, use built-in `Compare*Function`. Choose function depending on the wanted operator. The function takes a property expression on the left, and a value to compare (on the right).

/--php
use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction;
use Nextras\Orm\Collection\Functions\CountAggregateFunction;

// SELECT * FROM authors
// LEFT JOIN books ON (...)
// GROUP BY authors.id
// HAVING COUNT(books.id) > 2
$authorsCollection->findBy(
[
CompareGreaterThanFunction::class,
[CountAggregateFunction::class, 'books->id'],
2,
]
);
\--

You can nest these function calls together. This approach is very powerful and flexible, though, sometimes quite verbose. To ease this issue you may create own wrappers (not included in Orm!).

/--php
class Aggregate {
public static function count(string $expression): array {
return [CountAggregateFunction::class, $expression];
}
}
class Compare {
public static function gt(string $expression, $value): array {
return [
CompareGreaterThanFunction::class,
$expression,
$value,
];
}
}

// filters authors who have more than 2 books
// and sorts them by the count of their books descending
$authorsCollection
->findBy(Compare::gt(Aggregate::count('books->id'), 2))
->orderBy(Aggregate::count('books->id'), ICollection::DESC);
\--

Feel free to "share a feedback":https://github.com/nextras/orm/discussions/categories/show-and-tell about using aggregation functions.
10 changes: 5 additions & 5 deletions doc/collection-functions.texy
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ Collection Functions

Collection functions are a powerful extension point that will allow you to write a custom filtering or an ordering behavior.

The custom filtering or ordering require own implementation for each storage implementation; ideally you write it for both Dbal & array because you may need it in both cases (e.g. on persisted / unpersisted relationship collection). Both of the Dbal & Array implementation bring their own interface you have to directly implement.
The custom filtering or ordering requires own implementation for each storage implementation; ideally you write it for both Dbal & Array to allow use it for persisted and unpersisted collections. Both of the Dbal & Array implementation bring their own interface you have to implement directly.

/--div .[note]
**Why we have ArrayCollection and DbalCollection?**

Collection itself is independent from storage implementation. It is your choice if your collection function will work in both cases - for `ArrayCollection` and `DbalCollection`. Let us remind you, `ArrayCollection`s are commonly used in relationships when you set new entities into the relationship but until the relationship is persisted, you will work with an `ArrayCollection`.
\--

Collection functions can be used in `ICollection::findBy()` or `ICollection::orderBy()` methods. Collection function is used as an array argument, first array value is the function identifier (we recommend using function's class name) and then function's arguments as other array values. Collection functions may be used together, also nest together so you can reuse them.
Collection functions can be used in `ICollection::findBy()` or `ICollection::orderBy()` methods. A collection function is used as an array argument, the first value is the function identifier (it is recommended using function's class name) and then function's arguments as other array values. Collection functions may be used together, also nest together so you can reuse them.

/--php
// use directly a function call definition
// collection function call
$collection->findBy([MyFunction::class, 'arg1', 'arg2']);

// or compose & nest them together
// or compose & nest the calls together
// ICollection::OR is also a collection function
$collection->findBy(
[
Expand Down Expand Up @@ -75,7 +75,7 @@ final class LikeFunction implements IQueryBuilderFunction
array $args
): DbalExpressionResult
{
// $args is for example ['phone', '+420']
// $args is for example ['phone', '+420']
\assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1]));

$expression = $helper->processPropertyExpr($builder, $args[0]);
Expand Down
78 changes: 4 additions & 74 deletions doc/collection.texy
Original file line number Diff line number Diff line change
Expand Up @@ -28,80 +28,11 @@ Collection itself is **immutable**, all methods that modify the collection retur
Filtering
=========

Each collection can be filtered by an array of conditions. These conditions are passed as a parameter of the `findBy()` method. The array consists of entity property names and values. Keys can contain an optional operator. The default operator is equality operator. Let's see the example:

/--php
$books = $orm->books->findBy([
'author' => $author->id,
'publishedAt<=' => new DateTimeImmutable(),
]);
\--

Allowed operators are `=`, `!=`, `<=`, `<`, `>=` and `>`.

You can filter the collection using conditions over entity relationships. To filter collection by a relationship, use a *traversing expression*: it consists of the path delimited by `->` - the same arrow you use in PHP.

/--php
// find all books which were authored by Jon Snow
$orm->books->findBy(['author->name' => 'Jon Snow']);

// find all books which were not translated by Jon Snow
$orm->books->findBy(['translator->name!=' => 'Jon Snow']);
\--

The described syntax may be expanded to support the `OR` logical disjunction. Prepend the `ICollection::OR` constant as a first value of the filtering array:

/--php
// finds all books which were authored od translated by one specific person
$books = $orm->books->findBy([
ICollection::OR,
'author' => $person->id,
'translator' => $person->id,
]);
\--

You may nest the query array structure; use the same syntax repeatedly:

/--php
// find all man older than 10 years and woman younger than 10 years
$authors = $orm->author->findBy([
ICollection::OR,
[
ICollection::AND,
'age>=' => 10,
'sex' => 'male',
],
[
ICollection::AND,
'age<=' => 10,
'sex' => 'female',
],
]);
\--

The previous example can be shortened because the `AND` operator is the default logical operator.

/--php
// find all man older than 10 years and woman younger than 12 years
$authors = $orm->author->findBy([
ICollection::OR,
[
'age>=' => 10,
'gender' => 'male',
],
[
'age<=' => 12,
'gender' => 'female',
],
]);
\--

There are few restrictions:
- Filtering does not support any kind of aggregation. If you need to write more complex queries, [proxy your methods to a mapper layer | repository] or write custom [collection function | collection-functions].
- Relationship filtering is currently supported only over the persisted (non-virtual) properties. Support for virtual properties is unsupported.
Read more in [collection filtering chapter | collection-filtering].

--------------


Single result fetching
======================

Expand Down Expand Up @@ -144,7 +75,7 @@ $orm->books->findAll()->orderBy('title'); // ORDER BY title ASC
$orm->books->findAll()->orderBy('title', ICollection::DESC); // ORDER BY title DESC
\--

The `orderBy` method also accepts a property expression. See filtering in this chapter for further description.
The `orderBy` method also accepts a property expression. See [aggregation in collection filtering chapter | collection-filtering#toc-aggregation].

/--php
// ORDER BY age = 2
Expand All @@ -171,7 +102,7 @@ $orm->books->findAll()->orderBy([
Limiting
========

To limit the data collection, just use `limitBy()` method. The first argumnet is a limit, the second optional argument is a starting offset.
To limit the data collection, just use `limitBy()` method. The first argument is a limit, the second optional argument is a starting offset.

/--php
// get the last 10 published books
Expand Down Expand Up @@ -214,7 +145,6 @@ public function renderArticles($categoryId)
{/if}
\--


---------------


Expand Down
1 change: 1 addition & 0 deletions doc/menu.texy
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Reference Guide: .[h4]
- [Model | model]
- [Repository | repository]
- [Collection | collection]
- [Collection Filtering | collection-filtering]
- [Collection Functions | collection-functions]
- [Entity | entity]
- [Entity STI | entity-sti]
Expand Down

0 comments on commit a21e4ae

Please sign in to comment.