orm icon indicating copy to clipboard operation
orm copied to clipboard

Collection function syntax

Open stepapo opened this issue 3 months ago • 2 comments

It would be nice to call Collection Functions directly in property string if applicable.

Instead of doing: $authors->findBy([CountAggregateFunction::class, 'books->id', 2]), I would like to do: $authors->findBy(['books->id:count' => 2]).

Or even instead of: $authors->findBy([CompareGreaterThanFunction::class, [CountAggregateFunction::class, 'books->id'], 2]), just call: $authors->findBy(['books->id:count>' => 2]).

For filtering, this is actually easily achievable outside of library with custom ConditionParser:

class CustomConditionParser extends ConditionParser
{
   public function parsePropertyOperator(string $condition): array
   {
        // language=PhpRegExp
        $regexp = '#^(?P<path>' . self::PATH_REGEXP . ')(:(?P<function>\w+))?(?P<operator>!=|<=|>=|=|>|<|~)?$#';
        if (preg_match($regexp, $condition, $matches) !== 1) {
            return [CompareEqualsFunction::class, $condition];
        }
        $operator = $matches['operator'] ?? '=';
        $function = $matches['function'] ?? null;
        $condition = $this->parsePropertyFunction($condition);
        # TODO detect if custom function expects operator for comparison
        return match ($operator) {
            '=' => [CompareEqualsFunction::class, $condition],
            '!=' => [CompareNotEqualsFunction::class, $condition],
            '>=' => [CompareGreaterThanEqualsFunction::class, $condition],
            '>' => [CompareGreaterThanFunction::class, $condition],
            '<=' => [CompareSmallerThanEqualsFunction::class, $condition],
            '<' => [CompareSmallerThanFunction::class, $condition],
            '~' => [CompareLikeFunction::class, $condition],
            default => throw new InvalidStateException,
        };
    }

    public function parsePropertyFunction(string $propertyPath): array|string
    {
        // language=PhpRegExp
        $regexp = '#^(?P<path>' . self::PATH_REGEXP . ')(:(?P<function>\w+))?(?P<operator>!=|<=|>=|=|>|<|~)?$#';
        if (preg_match($regexp, $propertyPath, $matches) !== 1) {
            throw new InvalidArgumentException('Unsupported condition format.');
        }
        $path = $matches['path'];
        $function = $matches['function'] ?? null;
        return $function ? match ($function) {
            'avg' => [AvgAggregateFunction::class, $path],
            'count' => [CountAggregateFunction::class, $path],
            'max' => [MaxAggregateFunction::class, $path],
            'min' => [MinAggregateFunction::class, $path],
            'sum' => [SumAggregateFunction::class, $path],
            default => [$function, $path],
        } : $path;
    }
}

For sorting, I did not find a way to allow this without altering library: $authors->orderBy('books->id:count').

If this is not planned to be part of the library, I would at least like sorting in Collection to make use of ConditionParser, somehow like this:

public function orderBy($expression, string $direction = ICollection::ASC): ICollection
{
    $expression = $this->normalizeExpression($expression);
    ...
}

private function normalizeExpression(array|string $expression): array|string
{
    $parser = $this->mapper->getRepository()->getConditionParser();
    if (is_string($expression)) {
        return $parser->parsePropertyFunction($expression);
        # TODO support operator in sorting
    }
    if (isset($expression[0])) {
        return $expression;
    }
    $normalizedExpression = [];
    # TODO parse array expression
    return $normalizedExpression;
}

stepapo avatar Sep 18 '25 09:09 stepapo

If this is not planned to be part of the library,

Actually, this is the first proposal to make it "incorporated" this way. Definitely interesting.

Though at first sight, it goes a bit against the idea that the expression is similar to actual PHP (-> instead of .).

I would at least like sorting in Collection to make use of ConditionParser

That seems like a reasonable request, even though the order expression is not a "condition". As for the implementation, no idea if this is the way; I'll have to dig into it a bit more. Starting a PR would be a good option, I think.

hrach avatar Sep 18 '25 13:09 hrach

it goes a bit against the idea that the expression is similar to actual PHP (-> instead of .).

Perhaps books->id::count or count(books->id) would be better. Although the latter suggests support for functions accepting multiple properties/other arguments, which seems harder to achieve.

order expression is not a "condition".

In the end, even sorting already uses ConditionParser::parsePropertyExpr, but that does not seem like the right method to use for this.

Starting a PR would be a good option, I think.

I can look into it some more as well.

stepapo avatar Sep 19 '25 15:09 stepapo