Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Implemented like doctrine/doctrine-bundle's ContainerRepositoryFactory. #30

Closed
wants to merge 10 commits into from
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,61 @@ try {

return \Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($entityManager);
```


## Get repository in the container.

If you want get repository in the container. You can use `ContainerRepositoryFactory`:

```php
// config.php
use Roave\PsrContainerDoctrine\ORM\ContainerRepositoryFactory;

return [
'doctrine' => [
'configuration' => [
'orm_default' => [
// ...
'repository_factory' => ContainerRepositoryFactory::class,
],
],
],
'dependencies' => [
'factories' => [
// Add it by Laminas's ReflectionBasedAbstractFactory or custom callback.
// ContainerRepositoryFactory::class => ReflectionBasedAbstractFactory::class,
ContainerRepositoryFactory::class => function ($container) {
return new ContainerRepositoryFactory($container);
},

// Add custom repository factory.
FooRepository::class => FooRepositoryFactory::class,
],
],
];
```

That can inject some other service by container.

```php
use Roave\PsrContainerDoctrine\ORM\ContainerRepositoryFactory;
use Psr\Container\ContainerInterface;
use Doctrine\ORM\EntityManagerInterface;

$container = require 'config/container.php';

class FooRepository extends \Doctrine\ORM\EntityRepository {
public function __construct(
private SomeOtherService $service, // Inject some other service.
EntityManagerInterface $em,
ClassMetadata $class
) {
parent::__construct($em, $class);
}
}

$em = $container->get('doctrine.entity_manager.orm_default');
$repository = $em->getRepository(FooEntity::class);

var_dump($repository instanceof FooRepository); // true
```
86 changes: 86 additions & 0 deletions src/ORM/ContainerRepositoryFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Roave\PsrContainerDoctrine\ORM;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectRepository;
use Psr\Container\ContainerInterface;
use RuntimeException;

use function class_exists;
use function spl_object_hash;
use function sprintf;

final class ContainerRepositoryFactory implements RepositoryFactory
{
/** @var ObjectRepository[] */
private array $managedRepositories = [];

private ContainerInterface $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* {@inheritdoc}
*/
public function getRepository(EntityManagerInterface $entityManager, $entityName)
{
$metadata = $entityManager->getClassMetadata($entityName);
$customRepositoryName = $metadata->customRepositoryClassName;

if ($customRepositoryName !== null) {
// fetch from the container
if ($this->container->has($customRepositoryName)) {
$repository = $this->container->get($customRepositoryName);

if (! $repository instanceof ObjectRepository) {
throw new RuntimeException(sprintf(
'The service "%s" must implement ObjectRepository.',
$customRepositoryName
));
}

return $repository;
}

if (! class_exists($customRepositoryName)) {
throw new RuntimeException(sprintf(
'The "%s" entity has a repositoryClass set to "%s", ' .
'but this is not a valid class. Check your class naming. ',
$metadata->name,
$customRepositoryName,
));
}

// allow the repository to be created below
}

return $this->getOrCreateRepository($entityManager, $metadata);
}

private function getOrCreateRepository(
EntityManagerInterface $entityManager,
ClassMetadata $metadata
): ObjectRepository {
$repositoryHash = $metadata->getName() . spl_object_hash($entityManager);
if (isset($this->managedRepositories[$repositoryHash])) {
return $this->managedRepositories[$repositoryHash];
}

/**
* @var class-string<ObjectRepository> $repositoryClassName
* @psalm-suppress NoInterfaceProperties
*/
$repositoryClassName = $metadata->customRepositoryClassName
?? $entityManager->getConfiguration()->getDefaultRepositoryClassName();

return $this->managedRepositories[$repositoryHash] = new $repositoryClassName($entityManager, $metadata);
}
}
131 changes: 131 additions & 0 deletions test/ORM/ContainerRepositoryFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace RoaveTest\PsrContainerDoctrine\ORM;

use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Roave\PsrContainerDoctrine\ORM\ContainerRepositoryFactory;
use RuntimeException;
use stdClass;

class ContainerRepositoryFactoryTest extends TestCase
{
public function testGetRepositoryReturnsService(): void
{
/** @psalm-var class-string $fooEntity */
$fooEntity = 'Foo\FooEntity';
$em = $this->buildEntityManager([$fooEntity => 'my_repo']);
$repo = new EntityRepository($em, $em->getClassMetadata($fooEntity));
$container = $this->buildContainer(['my_repo' => $repo]);
$factory = new ContainerRepositoryFactory($container);

$this->assertSame($repo, $factory->getRepository($em, $fooEntity));
}

public function testServiceRepositoriesMustExtendObjectRepository(): void
{
/** @psalm-var class-string $fooEntity */
$fooEntity = 'Foo\FooEntity';
$em = $this->buildEntityManager([$fooEntity => 'my_repo']);
$repo = new stdClass();
$container = $this->buildContainer(['my_repo' => $repo]);
$factory = new ContainerRepositoryFactory($container);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('The service "my_repo" must implement ObjectRepository.');
$factory->getRepository($em, $fooEntity);
}

public function testCustomRepositoryIsNotAValidClass(): void
{
/** @psalm-var class-string $fooEntity */
$fooEntity = 'Foo\FooEntity';
$em = $this->buildEntityManager([$fooEntity => 'not_a_real_class']);
$container = $this->buildContainer([]);
$factory = new ContainerRepositoryFactory($container);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'The "Foo\FooEntity" entity has a repositoryClass set to "not_a_real_class", ' .
'but this is not a valid class. Check your class naming. '
);
$factory->getRepository($em, $fooEntity);
}

public function testGetRepositoryReturnsEntityRepository(): void
{
/** @psalm-var class-string $fooEntity */
$fooEntity = 'Foo\FooEntity';
$container = $this->createMock(ContainerInterface::class);
$container->method('has')->with($this->equalTo($fooEntity))->willReturn(false);

$em = $this->buildEntityManager([$fooEntity => null]);

$repositoryFactory = new ContainerRepositoryFactory($container);

$repository = $repositoryFactory->getRepository($em, $fooEntity);

$this->assertInstanceOf(EntityRepository::class, $repository);
$this->assertEquals($fooEntity, $repository->getClassName());

// test instance
$this->assertEquals($repository, $repositoryFactory->getRepository($em, $fooEntity));
}

/**
* @param array<class-string, string|null> $entityRepositoryClasses
*/
private function buildEntityManager(array $entityRepositoryClasses): EntityManagerInterface
{
/** @psalm-var array<string, ClassMetadata> $classMetadatas */
$classMetadatas = [];
foreach ($entityRepositoryClasses as $entityClass => $entityRepositoryClass) {
$metadata = new ClassMetadata($entityClass);

/** @psalm-suppress PropertyTypeCoercion */
$metadata->customRepositoryClassName = $entityRepositoryClass;

$classMetadatas[$entityClass] = $metadata;
}

$em = $this->getMockBuilder(EntityManagerInterface::class)->getMock();
$em->expects($this->any())
->method('getClassMetadata')
->willReturnCallback(static function (string $class) use ($classMetadatas): ClassMetadata {
return $classMetadatas[$class];
});

$em->expects($this->any())
->method('getConfiguration')
->willReturn(new Configuration());

return $em;
}

/**
* @param array<string, mixed> $services
*/
private function buildContainer(array $services): ContainerInterface
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->any())
->method('has')
->willReturnCallback(static function (string $id) use ($services): bool {
return isset($services[$id]);
});
$container->expects($this->any())
->method('get')
->willReturnCallback(static function (string $id) use ($services): object {
return $services[$id];
});

return $container;
}
}