From 1031c3895533a5b9d16300d4d2140c5ae55253fa Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 3 Jul 2024 18:55:19 +0400 Subject: [PATCH] feat: update `ActiveRepository` to have all the necessary methods; add examples --- src/Repository/ActiveRepository.php | 107 +++++++++++++++--- .../Repository/RepositoryWithActiveQuery.php | 35 ++++++ .../Repository/ActiveRepositoryTest.php | 64 +++++++++++ 3 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 tests/app/Repository/RepositoryWithActiveQuery.php create mode 100644 tests/src/Functional/Repository/ActiveRepositoryTest.php diff --git a/src/Repository/ActiveRepository.php b/src/Repository/ActiveRepository.php index f09e7b1..f8c7811 100644 --- a/src/Repository/ActiveRepository.php +++ b/src/Repository/ActiveRepository.php @@ -5,9 +5,22 @@ namespace Cycle\ActiveRecord\Repository; use Cycle\ActiveRecord\Facade; +use Cycle\ActiveRecord\Query\ActiveQuery; +use Cycle\ORM\ORMInterface; +use Cycle\ORM\RepositoryInterface; use Cycle\ORM\Select; /** + * ActiveRepository combines the advantages of the {@see RepositoryInterface} and the {@see ActiveQuery}: + * + * - Immutable by default + * - Can be associated with an entity, or not + * - No restriction "an entity can have only one repository" + * - Not a QueryBuilder entity, so it can follow a contract with a limited set of methods. + * - Organically used in the DI container + * + * @see self::forUpdate() as an example of immutabile method. + * * @internal * * @template-covariant TEntity of object @@ -18,25 +31,28 @@ class ActiveRepository private Select $select; /** - * @param class-string $class + * Redefine the constructor in the child class to set the default entity class: + * + * ```php + * public function __construct() + * { + * parent::__construct(User::class); + * } + * ``` + * + * @param class-string $role */ - public function __construct(string $class) + public function __construct(string $role) { $orm = Facade::getOrm(); - $this->select = new Select($orm, $class); + $this->select = $this->initSelect($orm, $role); } /** - * Get selector associated with the repository. + * Find entity by primary key. + * + * @note Limit of 1 will be added to the query. * - * @return Select - */ - public function select(): Select - { - return clone $this->select; - } - - /** * @return TEntity|null */ public function findByPK(mixed $id): ?object @@ -45,6 +61,10 @@ public function findByPK(mixed $id): ?object } /** + * Find a single record based on the given scope. + * + * @note Limit of 1 will be added to the query. + * * @return TEntity|null */ public function findOne(array $scope = [], array $orderBy = []): ?object @@ -61,10 +81,67 @@ public function findAll(array $scope = [], array $orderBy = []): iterable } /** - * ActiveQuery is always immutable by default. + * @return $this + * @mutation-free + */ + public function forUpdate(): static + { + return $this->with($this->select()->forUpdate()); + } + + /** + * Get a copy of the current selector associated with the repository. + * + * @note The method is final to prevent the selector from being modified. + * If you need to modify the default selector, consider using a constructor or {@see self::initSelect()}. + * + * @return Select + * @mutation-free + */ + final public function select(): Select + { + return clone $this->select; + } + + /** + * @param Select $select + * + * @return $this + */ + protected function with(Select $select): static + { + $repository = clone $this; + $repository->select = $select; + return $repository; + } + + /** + * Create a default selector for the repository. Used in the constructor. + * + * How to modify the default selector: + * + * ```php + * public function initSelect(ORMInterface $orm, string $role) + * { + * parent::initSelect($orm, $role)->where('deleted_at', '=', null); + * } + * ``` + * + * How to use {@see ActiveQuery} instead of {@see Select}: + * + * ```php + * protected function initSelect(ORMInterface $orm, string $role): Select + * { + * return new CustomActiveQuery($role); + * } + * ``` + * + * @param class-string $role + * + * @return Select */ - public function __clone() + protected function initSelect(ORMInterface $orm, string $role): Select { - $this->select = clone $this->select; + return new Select($orm, $role); } } diff --git a/tests/app/Repository/RepositoryWithActiveQuery.php b/tests/app/Repository/RepositoryWithActiveQuery.php new file mode 100644 index 0000000..7385554 --- /dev/null +++ b/tests/app/Repository/RepositoryWithActiveQuery.php @@ -0,0 +1,35 @@ + + */ +final class RepositoryWithActiveQuery extends ActiveRepository +{ + #[\Override] + public function __construct() + { + parent::__construct(User::class); + } + + #[\Override] + public function initSelect(ORMInterface $orm, string $role): Select + { + return new UserQuery(); + } + + public function withNameStartLetter(string $letter): static + { + return $this->with($this->select()->where(new Fragment('SUBSTRING(name, 1, 1) = ?', $letter))); + } +} diff --git a/tests/src/Functional/Repository/ActiveRepositoryTest.php b/tests/src/Functional/Repository/ActiveRepositoryTest.php new file mode 100644 index 0000000..5c46596 --- /dev/null +++ b/tests/src/Functional/Repository/ActiveRepositoryTest.php @@ -0,0 +1,64 @@ +findOne()); + } + + #[Test] + public function it_fetches_one_entity(): void + { + $repository = new ActiveRepository(User::class); + + $user = $repository->findOne(['id' => 2]); + + self::assertInstanceOf(User::class, $user); + self::assertSame(2, $user->id); + } + + #[Test] + public function it_fetches_one_entity_by_pk(): void + { + $repository = new ActiveRepository(User::class); + + $user = $repository->findByPK(2); + + self::assertInstanceOf(User::class, $user); + self::assertSame(2, $user->id); + } + + #[Test] + public function it_uses_custom_repository_with_active_query(): void + { + $repository = new RepositoryWithActiveQuery(); + $letter = 'J'; + + $user = $repository->withNameStartLetter($letter)->findOne(); + + self::assertInstanceOf(User::class, $user); + self::assertSame($letter, $user->name[0]); + } +}