Fork me on GitHub

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.

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, ArrayCollections 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.

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

// or compose & nest them together
// ICollection::OR is also a collection function
$collection->findBy(
    [
        ICollection::OR,
        [MyFunction::class, 'arg1', 'arg2'],
        [AnotherFunction::class, 'arg3'],
    ]
);

Functions are registered per repository. To do so, override Repository::createCollectionFunction($name) method to return your collection functions' instances.

class UsersRepository extends Nextras\Orm\Repository\Repository
{
    // ...
    public function createCollectionFunction(string $name)
    {
        if ($name === MyFunction::class) {
            return new MyFunction();
        } else {
            return parent::createCollectionFunction($name);
        }
    }
}

Dbal Functions#

Dbal collection functions have to implement Nextras\Orm\Collection\Functions\IQueryBuilderFunction interface. The only required method takes DbalQueryBuilderHelper for easier user input processing, QueryBuilder for creating table joins, and user input/function parameters.

Collection function has to return DbalExpressionResult object. This objects holds bits of SQL clauses which may be processed by Netras Dbal's SqlProcessor. Because you are not adding filters directly to QueryBuilder but rather return them, you may compose multiple collection functions together.

Let's see an example: a “Like” collection function; We want to compare any (property) expression through SQL's LIKE operator with a prefix comparison.

$users->findBy(
    [LikeFunction::class, 'phone', '+420']
);

In the example we would like to use the LikeFunction to filter users by their phones that start with +420 prefix. Our function will implement IQueryBuilderFunction interface and will receive $args with phone and +420. But, the first argument may be quite dynamic. What if user pass address->zipcode (e.g. a relationship expression) instead of simple phone, such expression would require table joins, and doing it all by hand would be difficult. Therefore Orm always passes a DbalQueryBuilderHelper that will handle all this for you. Use processPropertyExpr method to obtain a DbalResultExpression for the phone argument. Then just append needed SQL to the returned expression, e.g. LIKE operator with a Dbal's argument. That's all!

use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\DbalExpressionResult;
use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper;

final class LikeFunction implements IQueryBuilderFunction
{
    public function processQueryBuilderExpression(
        DbalQueryBuilderHelper $helper,
        QueryBuilder $builder,
        array $args
    ): DbalExpressionResult
    {
        \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1]));

        $expression = $helper->processPropertyExpr($builder, $args[0]);
        return $expression->append('LIKE %like_', $args[1]);
    }
}

The value that is processed by helper may not be just a column, but also an another expression returned from other collection function.

Array Functions#

Array collection functions have to implement Netras\Orm\Collection\Functions\IArrayFunction interface. It is different to Dbal's interface, because the filtering happens directly in PHP now. The only required method takes ArrayCollectionHelper for easier entity property processing, IEntity entity to check if it should (not) be filtered out, and user input/function parameters.

Array collection function returns a mixed value, the kind depends in which context it will be evaluated. The value will be interpreted as boolean in filtering context to indicate if the entity should be filtered out; the value will be used for comparison of two entities in ordering context (using the spaceship operator).

Let's see an example: a “Like” collection function; We want to compare any (property) expression to passed user-input value with a prefix comparison.

use Nette\Utils\Strings;
use Nextras\Orm\Collection\Functions\IArrayFunction;
use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper;
use Nextras\Orm\Entity\IEntity;

final class LikeFunction implements IArrayFunction
{
    public function processArrayExpression(ArrayCollectionHelper $helper, IEntity $entity, array $args)
    {
        \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1]));

        $value = $helper->getValue($entity, $args[0])->value;
        return Strings::startsWith($value, $args[1]);
    }

Our function implements the IArrayFunction interface and will receive helper objects & user-input. The same as in Dbal's example, the user property argument may vary from simple property access to traversing through relationship expression. Let's use helper to get the property expression value holder, then read the value from the holder. Finally, we simply compare the value with user-input argument by Nette's string helper.

PostgreSQL is case-sensitive, so you should apply the lower function & a functional index; These modifications are a case-specific, therefore the LIKE functionality is not provided in Orm by default.