Edit

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

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

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

// or compose & nest the calls 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 three arguments: DbalQueryBuilderHelper for easier user input processing, QueryBuilder for creating table joins, and a user input/function parameters.

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

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

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

In the example we would like to use the custom 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. The column/property argument may be quite dynamic too. What if user passes address->zipcode (i.e. a relationship expression) instead of simple phone, such expression would require table joins; doing it all by hand would be difficult. Therefore Orm provides DbalQueryBuilderHelper that will handle all this for you. Use processPropertyExpr method to obtain a DbalExpressionResult for the column/property 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
    {
        // $args is for example ['phone', '+420']
        \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 helper processed value may not be just a column, but also an another expression returned from other collection function.

If you like require more advance operation then appending the expression, construct new DbalExpressionResult object. Use Dbal's %ex modifier to expand already processed expression. Some properties of the original expression result may be lost by creating new expression result instance; if needed, pass the original's values as additional constructor parameters.

$expression = $helper->processPropertyExpr($builder, $args[0]);
return new DbalExpressionResult(['SUBSTRING(%ex, 0, %i) = %s', $expression->args, \strlen($args[1]), $args[1]]);

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.