-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
ArrayCollection
that does matching()
calls against the …
…private properties, not indirectly through the getters. (#105) * feat: add `ArrayCollection` that does `matching()` calls against the private properties, not indirectly through the getters. * traverse parents to find private property
- Loading branch information
Showing
11 changed files
with
453 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of rekalogika/collections package. | ||
* | ||
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev> | ||
* | ||
* For the full copyright and license information, please view the LICENSE file | ||
* that was distributed with this source code. | ||
*/ | ||
|
||
namespace Rekalogika\Domain\Collections; | ||
|
||
use Doctrine\Common\Collections\ArrayCollection as DoctrineArrayCollection; | ||
use Doctrine\Common\Collections\Collection; | ||
use Doctrine\Common\Collections\Criteria; | ||
use Doctrine\Common\Collections\Order; | ||
use Doctrine\Common\Collections\Selectable; | ||
use Rekalogika\Domain\Collections\Internal\DirectClosureExpressionVisitor; | ||
|
||
/** | ||
* @template TKey of array-key | ||
* @template T | ||
* @extends DoctrineArrayCollection<TKey,T> | ||
*/ | ||
class ArrayCollection extends DoctrineArrayCollection | ||
{ | ||
/** | ||
* @psalm-return Collection<TKey,T>&Selectable<TKey,T> | ||
*/ | ||
public function matching(Criteria $criteria): Collection&Selectable | ||
{ | ||
$expr = $criteria->getWhereExpression(); | ||
$filtered = $this->toArray(); | ||
|
||
if ($expr) { | ||
$visitor = new DirectClosureExpressionVisitor(); | ||
/** @var \Closure(T):bool */ | ||
$filter = $visitor->dispatch($expr); | ||
$filtered = array_filter($filtered, $filter); | ||
} | ||
|
||
$orderings = $criteria->orderings(); | ||
|
||
if ($orderings) { | ||
$next = null; | ||
foreach (array_reverse($orderings) as $field => $ordering) { | ||
/** @var \Closure(mixed,mixed):int */ | ||
$next = DirectClosureExpressionVisitor::sortByField($field, $ordering === Order::Descending ? -1 : 1, $next); | ||
} | ||
|
||
uasort($filtered, $next); | ||
} | ||
|
||
$offset = $criteria->getFirstResult(); | ||
$length = $criteria->getMaxResults(); | ||
|
||
if ($offset !== null && $offset > 0 || $length !== null && $length > 0) { | ||
$filtered = array_slice($filtered, (int) $offset, $length, true); | ||
} | ||
|
||
return $this->createFrom($filtered); | ||
} | ||
} |
200 changes: 200 additions & 0 deletions
200
packages/collections-domain/src/Internal/DirectClosureExpressionVisitor.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of rekalogika/collections package. | ||
* | ||
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev> | ||
* | ||
* For the full copyright and license information, please view the LICENSE file | ||
* that was distributed with this source code. | ||
*/ | ||
|
||
namespace Rekalogika\Domain\Collections\Internal; | ||
|
||
use Doctrine\Common\Collections\Expr\Comparison; | ||
use Rekalogika\Contracts\Collections\Exception\UnexpectedValueException; | ||
|
||
use Closure; | ||
use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
use Doctrine\Common\Collections\Expr\Value; | ||
use RuntimeException; | ||
|
||
use function explode; | ||
use function in_array; | ||
use function is_array; | ||
use function is_scalar; | ||
use function iterator_to_array; | ||
use function str_contains; | ||
use function str_ends_with; | ||
use function str_starts_with; | ||
|
||
/** | ||
* Copied from ClosureExpressionVisitor from doctrine/collections package. | ||
*/ | ||
class DirectClosureExpressionVisitor extends ExpressionVisitor | ||
{ | ||
/** | ||
* Accesses the field of a given object. This field has to be public | ||
* directly or indirectly (through an accessor get*, is*, or a magic | ||
* method, __get, __call). | ||
* | ||
* @param object|mixed[] $object | ||
* | ||
* @return mixed | ||
*/ | ||
public static function getObjectFieldValue(object|array $object, string $field) | ||
{ | ||
if (str_contains($field, '.')) { | ||
[$field, $subField] = explode('.', $field, 2); | ||
$object = self::getObjectFieldValue($object, $field); | ||
|
||
return self::getObjectFieldValue($object, $subField); | ||
} | ||
|
||
if (is_array($object)) { | ||
return $object[$field]; | ||
} | ||
|
||
$reflection = new \ReflectionObject($object); | ||
|
||
do { | ||
if ($reflection->hasProperty($field)) { | ||
$property = $reflection->getProperty($field); | ||
$property->setAccessible(true); | ||
|
||
return $property->getValue($object); | ||
} | ||
} while ($reflection = $reflection->getParentClass()); | ||
|
||
throw new UnexpectedValueException('Unknown field ' . $field); | ||
} | ||
|
||
/** | ||
* Helper for sorting arrays of objects based on multiple fields + orientations. | ||
* | ||
* @return Closure | ||
*/ | ||
public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null) | ||
{ | ||
if (!$next) { | ||
$next = static fn (): int => 0; | ||
} | ||
|
||
return static function ($a, $b) use ($name, $next, $orientation): int { | ||
$aValue = static::getObjectFieldValue($a, $name); | ||
|
||
$bValue = static::getObjectFieldValue($b, $name); | ||
|
||
if ($aValue === $bValue) { | ||
return $next($a, $b); | ||
} | ||
|
||
return ($aValue > $bValue ? 1 : -1) * $orientation; | ||
}; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function walkComparison(Comparison $comparison) | ||
{ | ||
$field = $comparison->getField(); | ||
$value = $comparison->getValue()->getValue(); | ||
|
||
return match ($comparison->getOperator()) { | ||
Comparison::EQ => static fn ($object): bool => static::getObjectFieldValue($object, $field) === $value, | ||
Comparison::NEQ => static fn ($object): bool => static::getObjectFieldValue($object, $field) !== $value, | ||
Comparison::LT => static fn ($object): bool => static::getObjectFieldValue($object, $field) < $value, | ||
Comparison::LTE => static fn ($object): bool => static::getObjectFieldValue($object, $field) <= $value, | ||
Comparison::GT => static fn ($object): bool => static::getObjectFieldValue($object, $field) > $value, | ||
Comparison::GTE => static fn ($object): bool => static::getObjectFieldValue($object, $field) >= $value, | ||
Comparison::IN => static function ($object) use ($field, $value): bool { | ||
$fieldValue = static::getObjectFieldValue($object, $field); | ||
|
||
return in_array($fieldValue, $value, is_scalar($fieldValue)); | ||
}, | ||
Comparison::NIN => static function ($object) use ($field, $value): bool { | ||
$fieldValue = static::getObjectFieldValue($object, $field); | ||
|
||
return !in_array($fieldValue, $value, is_scalar($fieldValue)); | ||
}, | ||
Comparison::CONTAINS => static fn ($object): bool => str_contains((string) static::getObjectFieldValue($object, $field), (string) $value), | ||
Comparison::MEMBER_OF => static function ($object) use ($field, $value): bool { | ||
$fieldValues = static::getObjectFieldValue($object, $field); | ||
|
||
if (!is_array($fieldValues)) { | ||
$fieldValues = iterator_to_array($fieldValues); | ||
} | ||
|
||
return in_array($value, $fieldValues, true); | ||
}, | ||
Comparison::STARTS_WITH => static fn ($object): bool => str_starts_with((string) static::getObjectFieldValue($object, $field), (string) $value), | ||
Comparison::ENDS_WITH => static fn ($object): bool => str_ends_with((string) static::getObjectFieldValue($object, $field), (string) $value), | ||
default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()), | ||
}; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function walkValue(Value $value) | ||
{ | ||
return $value->getValue(); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function walkCompositeExpression(CompositeExpression $expr) | ||
{ | ||
$expressionList = []; | ||
|
||
foreach ($expr->getExpressionList() as $child) { | ||
$expressionList[] = $this->dispatch($child); | ||
} | ||
|
||
return match ($expr->getType()) { | ||
CompositeExpression::TYPE_AND => $this->andExpressions($expressionList), | ||
CompositeExpression::TYPE_OR => $this->orExpressions($expressionList), | ||
CompositeExpression::TYPE_NOT => $this->notExpression($expressionList), | ||
default => throw new RuntimeException('Unknown composite ' . $expr->getType()), | ||
}; | ||
} | ||
|
||
/** @param callable[] $expressions */ | ||
private function andExpressions(array $expressions): Closure | ||
{ | ||
return static function ($object) use ($expressions): bool { | ||
foreach ($expressions as $expression) { | ||
if (!$expression($object)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}; | ||
} | ||
|
||
/** @param callable[] $expressions */ | ||
private function orExpressions(array $expressions): Closure | ||
{ | ||
return static function ($object) use ($expressions): bool { | ||
foreach ($expressions as $expression) { | ||
if ($expression($object)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
}; | ||
} | ||
|
||
/** @param callable[] $expressions */ | ||
private function notExpression(array $expressions): Closure | ||
{ | ||
return static fn ($object) => !$expressions[0]($object); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.