From 1d6f654c2f6912dff1da8988b70bc9f892049e3a Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 12 Feb 2025 12:43:31 +0200 Subject: [PATCH] collection methods --- application/Espo/ORM/EntityCollection.php | 69 ++++++++++++++++ application/Espo/ORM/SthCollection.php | 63 ++++++++++++++- .../Espo/ORM/SthCollectionTest.php | 22 ++++++ tests/unit/Espo/ORM/CollectionTest.php | 79 +++++++++++++++++++ 4 files changed, 231 insertions(+), 2 deletions(-) diff --git a/application/Espo/ORM/EntityCollection.php b/application/Espo/ORM/EntityCollection.php index e896f85341..7fce3cd4b3 100644 --- a/application/Espo/ORM/EntityCollection.php +++ b/application/Espo/ORM/EntityCollection.php @@ -37,6 +37,7 @@ use SeekableIterator; use RuntimeException; use OutOfBoundsException; use InvalidArgumentException; +use Closure; /** * A standard collection of entities. It allocates a memory for all entities. @@ -399,4 +400,72 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess, return $obj; } + + /** + * Filter. + * + * @param Closure(TEntity): bool $callback A filter callback. + * @return self A filtered collection. A new instance. + * @since 9.1.0 + */ + public function filter(Closure $callback): self + { + $newList = []; + + foreach ($this as $entity) { + if ($callback($entity)) { + $newList[] = $entity; + } + } + + return new EntityCollection($newList, $this->entityType, $this->entityFactory); + } + + /** + * Sort. + * + * @param Closure(TEntity, TEntity): int $callback The comparison function. + * @return self A sorted collection. A new instance. + * @since 9.1.0 + */ + public function sort(Closure $callback): self + { + $newList = [...$this]; + + usort($newList, $callback); + + return new EntityCollection($newList, $this->entityType, $this->entityFactory); + } + + /** + * Reverse. + * + * @return self A reversed collection. + * @since 9.1.0 + */ + public function reverse(): self + { + $newList = array_reverse([...$this]); + + return new EntityCollection($newList, $this->entityType, $this->entityFactory); + } + + /** + * Find. + * + * @param Closure(TEntity): bool $callback A filter callback. + * @return ?TEntity + * @since 9.1.0 + * @noinspection PhpDocSignatureInspection + */ + public function find(Closure $callback): ?Entity + { + foreach ($this as $entity) { + if ($callback($entity)) { + return $entity; + } + } + + return null; + } } diff --git a/application/Espo/ORM/SthCollection.php b/application/Espo/ORM/SthCollection.php index d82451b125..d450c171d6 100644 --- a/application/Espo/ORM/SthCollection.php +++ b/application/Espo/ORM/SthCollection.php @@ -30,7 +30,6 @@ namespace Espo\ORM; use Espo\ORM\Query\Select as SelectQuery; - use IteratorAggregate; use Countable; use Traversable; @@ -38,6 +37,7 @@ use PDO; use PDOStatement; use RuntimeException; use LogicException; +use Closure; /** * Reasonable to use when selecting a large number of records. @@ -144,7 +144,7 @@ class SthCollection implements Collection, IteratorAggregate, Countable $list = []; foreach ($this as $entity) { - $list[] = $entity->getValueMap();; + $list[] = $entity->getValueMap(); } return $list; @@ -170,6 +170,7 @@ class SthCollection implements Collection, IteratorAggregate, Countable * Create from a query. * * @return self + * @internal */ public static function fromQuery(SelectQuery $query, EntityManager $entityManager): self { @@ -192,6 +193,7 @@ class SthCollection implements Collection, IteratorAggregate, Countable * Create from an SQL. * * @return self + * @internal */ public static function fromSql(string $entityType, string $sql, EntityManager $entityManager): self { @@ -203,4 +205,61 @@ class SthCollection implements Collection, IteratorAggregate, Countable return $obj; } + + /** + * Filter. + * + * @param Closure(TEntity): bool $callback A filter callback. + * @return EntityCollection A filtered collection. + * @since 9.1.0 + */ + public function filter(Closure $callback): EntityCollection + { + return $this->toEntityCollection()->filter($callback); + } + + /** + * Sort. + * + * @param Closure(TEntity, TEntity): int $callback The comparison function. + * @return EntityCollection A sorted collection. A new instance. + * @since 9.1.0 + */ + public function sort(Closure $callback): EntityCollection + { + return $this->toEntityCollection()->sort($callback); + } + + /** + * Reverse. + * + * @return EntityCollection A reversed collection. + * @since 9.1.0 + */ + public function reverse(): EntityCollection + { + return $this->toEntityCollection()->reverse(); + } + + /** + * Find. + * + * @param Closure(TEntity): bool $callback A filter callback. + * @return ?TEntity + * @since 9.1.0 + * @noinspection PhpDocSignatureInspection + */ + public function find(Closure $callback): ?Entity + { + return $this->toEntityCollection()->find($callback); + } + + /** + * @return EntityCollection + */ + private function toEntityCollection(): EntityCollection + { + /** @var EntityCollection */ + return (new EntityCollection([...$this], $this->entityType)); + } } diff --git a/tests/integration/Espo/ORM/SthCollectionTest.php b/tests/integration/Espo/ORM/SthCollectionTest.php index 4654fc185e..363bcdedc4 100644 --- a/tests/integration/Espo/ORM/SthCollectionTest.php +++ b/tests/integration/Espo/ORM/SthCollectionTest.php @@ -29,6 +29,7 @@ namespace tests\integration\Espo\ORM; +use Espo\Modules\Crm\Entities\Account; use Espo\ORM\EntityManager; use Espo\ORM\SthCollection; use tests\integration\Core\BaseTestCase; @@ -234,4 +235,25 @@ class SthCollectionTest extends BaseTestCase $this->assertEquals(2, $count); } + + public function testMethods(): void + { + $e1 = $this->getEntityManager()->createEntity(Account::ENTITY_TYPE, ['name' => '1']); + $e2 = $this->getEntityManager()->createEntity(Account::ENTITY_TYPE, ['name' => '2']); + $e3 = $this->getEntityManager()->createEntity(Account::ENTITY_TYPE, ['name' => '3']); + $e4 = $this->getEntityManager()->createEntity(Account::ENTITY_TYPE, ['name' => '4']); + + $collection = $this->getEntityManager() + ->getRDBRepositoryByClass(Account::class) + ->sth() + ->order('name') + ->find(); + + $filtered = $collection->filter(function ($e) use ($e2, $e3) { + return $e->getId() !== $e2->getId() && $e->getId() !== $e3->getId(); + }); + + $this->assertEquals([$e1->getId(), $e4->getId()], array_map(fn ($it) => $it->getId(), [...$filtered])); + $this->assertEquals($collection->getEntityType(), $filtered->getEntityType()); + } } diff --git a/tests/unit/Espo/ORM/CollectionTest.php b/tests/unit/Espo/ORM/CollectionTest.php index d9f85b85fa..000962ae32 100644 --- a/tests/unit/Espo/ORM/CollectionTest.php +++ b/tests/unit/Espo/ORM/CollectionTest.php @@ -32,12 +32,17 @@ namespace tests\unit\Espo\ORM; use Espo\ORM\BaseEntity; use Espo\ORM\Defs; use Espo\ORM\Defs\DefsData; +use Espo\ORM\Entity; use Espo\ORM\EntityCollection; +use Espo\ORM\Executor\QueryExecutor; use Espo\ORM\Metadata; use Espo\ORM\MetadataDataProvider; use Espo\Core\ORM\EntityManager; +use Espo\ORM\SthCollection; +use PDOStatement; use PHPUnit\Framework\TestCase; +use SplObjectStorage; require_once 'tests/unit/testData/DB/Entities.php'; @@ -117,4 +122,78 @@ class CollectionTest extends TestCase $this->assertEquals(1, $collection->count()); } + + public function testFilter(): void + { + $e1 = $this->createMock(Entity::class); + $e2 = $this->createMock(Entity::class); + $e3 = $this->createMock(Entity::class); + $e4 = $this->createMock(Entity::class); + + $collection = new EntityCollection([$e1, $e2, $e3, $e4], 'Account'); + + $filtered = $collection->filter(function ($e) use ($e2, $e3) { + return $e !== $e2 && $e !== $e3; + }); + + $this->assertEquals([$e1, $e2], [...$filtered]); + $this->assertEquals($collection->getEntityType(), $filtered->getEntityType()); + } + + public function testSort(): void + { + $e1 = $this->createMock(Entity::class); + $e2 = $this->createMock(Entity::class); + $e3 = $this->createMock(Entity::class); + + $map = new SplObjectStorage(); + + $map[$e1] = 3; + $map[$e2] = 2; + $map[$e3] = 1; + + $collection = new EntityCollection([$e1, $e2, $e3], 'Account'); + + $sorted = $collection->sort(function ($e1, $e2) use ($map) { + return $map[$e1] - $map[$e2]; + }); + + $this->assertEquals([$e3, $e2, $e1], [...$sorted]); + $this->assertEquals($collection->getEntityType(), $sorted->getEntityType()); + } + + public function testFind(): void + { + $e1 = $this->createMock(Entity::class); + $e2 = $this->createMock(Entity::class); + $e3 = $this->createMock(Entity::class); + + $collection = new EntityCollection([$e1, $e2, $e3]); + + $e = $collection->find(function ($e) use ($e2) { + return $e === $e2; + }); + + $this->assertSame($e2, $e); + + $e = $collection->find(function ($e) use ($e2) { + return $e === 0; + }); + + $this->assertNull($e); + } + + public function testReverse(): void + { + $e1 = $this->createMock(Entity::class); + $e2 = $this->createMock(Entity::class); + $e3 = $this->createMock(Entity::class); + + $collection = new EntityCollection([$e1, $e2, $e3], 'Account'); + + $reversed = $collection->reverse(); + + $this->assertEquals([$e3, $e2, $e1], [...$reversed]); + $this->assertEquals($collection->getEntityType(), $reversed->getEntityType()); + } }