Skip to content

Commit

Permalink
feat: update ActiveRepository to have all the necessary methods; ad…
Browse files Browse the repository at this point in the history
…d examples
  • Loading branch information
roxblnfk committed Jul 3, 2024
1 parent ad7c0de commit 1031c38
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 15 deletions.
107 changes: 92 additions & 15 deletions src/Repository/ActiveRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,25 +31,28 @@ class ActiveRepository
private Select $select;

/**
* @param class-string<TEntity> $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<TEntity> $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);

Check failure on line 48 in src/Repository/ActiveRepository.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

InvalidPropertyAssignmentValue

src/Repository/ActiveRepository.php:48:25: InvalidPropertyAssignmentValue: $this->select with declared type 'Cycle\ORM\Select<TEntity:Cycle\ActiveRecord\Repository\ActiveRepository as object>' cannot be assigned type 'Cycle\ORM\Select<object>' (see https://psalm.dev/145)
}

/**
* Get selector associated with the repository.
* Find entity by primary key.
*
* @note Limit of 1 will be added to the query.
*
* @return Select<TEntity>
*/
public function select(): Select
{
return clone $this->select;
}

/**
* @return TEntity|null
*/
public function findByPK(mixed $id): ?object
Expand All @@ -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
Expand All @@ -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

Check failure on line 87 in src/Repository/ActiveRepository.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

PossiblyUnusedMethod

src/Repository/ActiveRepository.php:87:21: PossiblyUnusedMethod: Cannot find any calls to method Cycle\ActiveRecord\Repository\ActiveRepository::forUpdate (see https://psalm.dev/087)
{
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<TEntity>
* @mutation-free
*/
final public function select(): Select
{
return clone $this->select;
}

/**
* @param Select<TEntity> $select
*
* @return $this
*/
protected function with(Select $select): static

Check failure on line 111 in src/Repository/ActiveRepository.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Template type TEntity is declared as covariant, but occurs in contravariant position in parameter select of method Cycle\ActiveRecord\Repository\ActiveRepository::with().
{
$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<TEntity> $role
*
* @return Select<TEntity>
*/
public function __clone()
protected function initSelect(ORMInterface $orm, string $role): Select

Check failure on line 143 in src/Repository/ActiveRepository.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Template type TEntity is declared as covariant, but occurs in contravariant position in parameter role of method Cycle\ActiveRecord\Repository\ActiveRepository::initSelect().
{
$this->select = clone $this->select;
return new Select($orm, $role);
}
}
35 changes: 35 additions & 0 deletions tests/app/Repository/RepositoryWithActiveQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Cycle\App\Repository;

use Cycle\ActiveRecord\Repository\ActiveRepository;
use Cycle\App\Entity\User;
use Cycle\App\Query\UserQuery;
use Cycle\Database\Injection\Fragment;
use Cycle\ORM\ORMInterface;
use Cycle\ORM\Select;

/**
* @extends ActiveRepository<User>
*/
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)));
}
}
64 changes: 64 additions & 0 deletions tests/src/Functional/Repository/ActiveRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Cycle\Tests\Functional\Repository;

use Cycle\ActiveRecord\Facade;
use Cycle\ActiveRecord\Repository\ActiveRepository;
use Cycle\App\Entity\User;
use Cycle\App\Repository\RepositoryWithActiveQuery;
use Cycle\Database\Database;
use Cycle\Database\DatabaseManager;
use Cycle\Tests\Functional\DatabaseTestCase;
use PHPUnit\Framework\Attributes\Test;

final class ActiveRepositoryTest extends DatabaseTestCase
{
#[Test]
public function it_extends_repository_constructor(): void
{
$repository = new class extends ActiveRepository {
public function __construct()
{
parent::__construct(User::class);
}
};

self::assertInstanceOf(User::class, $repository->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]);
}
}

0 comments on commit 1031c38

Please sign in to comment.