Skip to content

Commit 3f65990

Browse files
committed
annotated repository factory & unit tests WIP
1 parent 5803a8e commit 3f65990

9 files changed

Lines changed: 338 additions & 18 deletions

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"laminas/laminas-servicemanager": "^3.10",
2525
"doctrine/annotations": "^1.13",
2626
"doctrine/cache": "^1.13",
27-
"laminas/laminas-dependency-plugin": "^2.2"
27+
"doctrine/orm" : "^2.11"
2828
},
2929
"require-dev": {
3030
"phpunit/phpunit": "^9.5",
@@ -41,6 +41,7 @@
4141
}
4242
},
4343
"suggest": {
44-
"doctrine/cache": "To cache the result of processing the annotations and speed-up your application"
44+
"doctrine/cache": "To cache the result of processing the annotations and speed-up your application.",
45+
"doctrine/orm": "To be able to inject doctrine repositories."
4546
}
4647
}

src/Annotation/Entity.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Dot\AnnotatedServices\Annotation;
6+
7+
use Doctrine\Common\Annotations\Annotation;
8+
use Doctrine\Common\Annotations\Annotation\Target;
9+
use Doctrine\Common\Annotations\Annotation\Required;
10+
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
11+
12+
/**
13+
* Class Entity
14+
* @package Dot\AnnotatedServices\Annotation
15+
*
16+
* @Annotation
17+
* @NamedArgumentConstructor
18+
* @Target({"CLASS"})
19+
*/
20+
class Entity
21+
{
22+
/** @Required */
23+
private string $name;
24+
25+
/**
26+
* Entity constructor.
27+
* @param string $name
28+
*/
29+
public function __construct(string $name)
30+
{
31+
$this->name = $name;
32+
}
33+
34+
/**
35+
* @return string
36+
*/
37+
public function getName(): string
38+
{
39+
return $this->name;
40+
}
41+
}

src/Exception/RuntimeException.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,36 @@
1515
*/
1616
class RuntimeException extends \RuntimeException implements ExceptionInterface
1717
{
18+
public static function classNotFound(string $requestedName): self
19+
{
20+
return new self(sprintf(
21+
'Defined injectable service "%s" could not be found in container or as a class.',
22+
$requestedName
23+
));
24+
}
1825

26+
public static function doesNotExtend(string $class): self
27+
{
28+
return new self(sprintf('Class has to extend "%s".', $class));
29+
}
30+
31+
public static function annotationNotFound(string $annotation, string $class, string $factory): self
32+
{
33+
return new self(sprintf(
34+
'You need to use the "%s" annotation in "%s" class so that the "%s" can create it.',
35+
$annotation,
36+
$class,
37+
$factory
38+
));
39+
}
40+
41+
public static function invalidAnnotation(string $requestedName): self
42+
{
43+
return new self(sprintf(
44+
'Annotated factories can only be used with services that are identified by their FQCN. ' .
45+
'Provided "%s" service name is not a valid class.',
46+
$requestedName
47+
));
48+
}
1949
}
50+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Dot\AnnotatedServices\Factory;
6+
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\ORM\EntityRepository;
9+
use Dot\AnnotatedServices\Annotation\Entity;
10+
use Dot\AnnotatedServices\Exception\RuntimeException;
11+
use Psr\Container\ContainerExceptionInterface;
12+
use Psr\Container\ContainerInterface;
13+
use Psr\Container\NotFoundExceptionInterface;
14+
use ReflectionException;
15+
use ReflectionClass;
16+
17+
/**
18+
* Class AnnotatedRepositoryFactory
19+
* @package Dot\AnnotatedServices\Factory
20+
*/
21+
class AnnotatedRepositoryFactory extends AbstractAnnotatedFactory
22+
{
23+
/**
24+
* @param ContainerInterface $container
25+
* @param $requestedName
26+
* @return EntityRepository
27+
* @throws ContainerExceptionInterface
28+
* @throws NotFoundExceptionInterface
29+
* @throws ReflectionException
30+
* @throws RuntimeException
31+
*/
32+
public function __invoke(ContainerInterface $container, $requestedName): EntityRepository
33+
{
34+
return $this->createObject($container, $requestedName);
35+
}
36+
37+
/**
38+
* @param ContainerInterface $container
39+
* @param $requestedName
40+
* @return EntityRepository
41+
* @throws ContainerExceptionInterface
42+
* @throws NotFoundExceptionInterface
43+
* @throws ReflectionException
44+
*/
45+
public function createObject(ContainerInterface $container, $requestedName): EntityRepository
46+
{
47+
if (! class_exists($requestedName)) {
48+
throw RuntimeException::classNotFound($requestedName);
49+
}
50+
51+
$reflectionClass = new ReflectionClass($requestedName);
52+
if (! $reflectionClass->isSubclassOf(EntityRepository::class)) {
53+
throw RuntimeException::doesNotExtend(EntityRepository::class);
54+
}
55+
56+
$annotationReader = $this->createAnnotationReader($container);
57+
$entity = $annotationReader->getClassAnnotation($reflectionClass, Entity::class);
58+
if (! $entity) {
59+
throw RuntimeException::annotationNotFound(Entity::class, $requestedName, static::class);
60+
}
61+
62+
$entityManager = $container->get(EntityManagerInterface::class);
63+
return $entityManager->getRepository($entity->getName());
64+
}
65+
}

src/Factory/AnnotatedServiceFactory.php

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
use Dot\AnnotatedServices\Annotation\Inject;
1313
use Dot\AnnotatedServices\Exception\InvalidArgumentException;
1414
use Dot\AnnotatedServices\Exception\RuntimeException;
15+
use Psr\Container\ContainerExceptionInterface;
1516
use Psr\Container\ContainerInterface;
17+
use Psr\Container\NotFoundExceptionInterface;
18+
use ReflectionClass;
19+
use ReflectionException;
20+
use ReflectionMethod;
1621

1722
/**
1823
* Class AnnotatedServiceFactory
@@ -23,7 +28,10 @@ class AnnotatedServiceFactory extends AbstractAnnotatedFactory
2328
/**
2429
* @param ContainerInterface $container
2530
* @param $requestedName
26-
* @return null
31+
* @return mixed
32+
* @throws ContainerExceptionInterface
33+
* @throws NotFoundExceptionInterface
34+
* @throws ReflectionException
2735
*/
2836
public function __invoke(ContainerInterface $container, $requestedName)
2937
{
@@ -34,33 +42,31 @@ public function __invoke(ContainerInterface $container, $requestedName)
3442
* @param ContainerInterface $container
3543
* @param $requestedName
3644
* @return mixed
45+
* @throws ContainerExceptionInterface
46+
* @throws NotFoundExceptionInterface
47+
* @throws ReflectionException
3748
*/
3849
public function createObject(ContainerInterface $container, $requestedName)
3950
{
4051
if (!class_exists($requestedName)) {
41-
throw new RuntimeException(sprintf(
42-
'Annotated factories can only be used with services that are identified by their FQCN. ' .
43-
'Provided "%s" service name is not a valid class.',
44-
$requestedName
45-
));
52+
throw RuntimeException::classNotFound($requestedName);
4653
}
4754

4855
$service = null;
4956

5057
$annotationReader = $this->createAnnotationReader($container);
51-
$refClass = new \ReflectionClass($requestedName);
58+
$refClass = new ReflectionClass($requestedName);
5259
$constructor = $refClass->getConstructor();
5360
if ($constructor === null) {
5461
$service = new $requestedName();
5562
} else {
5663
$inject = $annotationReader->getMethodAnnotation($constructor, Inject::class);
5764
if ($inject === null && $constructor->getNumberOfRequiredParameters() > 0) {
58-
throw new RuntimeException(sprintf(
59-
'You need to use the "%s" annotation in "%s" constructor so that the "%s" can create it.',
65+
throw RuntimeException::annotationNotFound(
6066
Inject::class,
6167
$requestedName,
6268
static::class
63-
));
69+
);
6470
}
6571

6672
$services = [];
@@ -71,7 +77,7 @@ public function createObject(ContainerInterface $container, $requestedName)
7177
$service = new $requestedName(...$services);
7278
}
7379

74-
$methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC);
80+
$methods = $refClass->getMethods(ReflectionMethod::IS_PUBLIC);
7581
foreach ($methods as $method) {
7682
$inject = $annotationReader->getMethodAnnotation($method, Inject::class);
7783
if ($inject) {
@@ -87,6 +93,8 @@ public function createObject(ContainerInterface $container, $requestedName)
8793
* @param ContainerInterface $container
8894
* @param Inject $inject
8995
* @return array
96+
* @throws ContainerExceptionInterface
97+
* @throws NotFoundExceptionInterface
9098
*/
9199
protected function getServicesToInject(ContainerInterface $container, Inject $inject): array
92100
{
@@ -106,10 +114,7 @@ protected function getServicesToInject(ContainerInterface $container, Inject $in
106114
} elseif (class_exists($serviceKey)) {
107115
$service = new $serviceKey();
108116
} else {
109-
throw new RuntimeException(sprintf(
110-
'Defined injectable service "%s" could not be found in container or as a class.',
111-
$serviceKey
112-
));
117+
throw RuntimeException::classNotFound($serviceKey);
113118
}
114119

115120
$services[] = empty($parts) ? $service : $this->readKeysFromArray($parts, $service);
@@ -126,7 +131,7 @@ protected function getServicesToInject(ContainerInterface $container, Inject $in
126131
protected function readKeysFromArray(array $keys, $array)
127132
{
128133
$key = array_shift($keys);
129-
// When one of the provided keys is not found, thorw an exception
134+
// When one of the provided keys is not found, throw an exception
130135
if (!isset($array[$key])) {
131136
throw new InvalidArgumentException(sprintf(
132137
'The key "%s" provided in the dotted notation could not be found in the array service',
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace DotTest\AnnotatedServices;
4+
5+
use Doctrine\Common\Annotations\Reader;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Doctrine\ORM\EntityRepository;
8+
use Dot\AnnotatedServices\Annotation\Entity;
9+
use Dot\AnnotatedServices\Exception\RuntimeException;
10+
use DotTest\AnnotatedServices\Stubs\TestRepository;
11+
use PHPUnit\Framework\TestCase;
12+
use Psr\Container\ContainerInterface;
13+
use Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory as Subject;
14+
15+
/**
16+
* Class AnnotatedRepositoryFactoryTest
17+
* @package DotTest\AnnotatedServices
18+
*/
19+
class AnnotatedRepositoryFactoryTest extends TestCase
20+
{
21+
private ContainerInterface $container;
22+
23+
private Subject $subject;
24+
25+
private Reader $annotationReader;
26+
27+
public function setUp(): void
28+
{
29+
parent::setUp();
30+
31+
$this->container = $this->createMock(ContainerInterface::class);
32+
$this->annotationReader = $this->createMock(Reader::class);
33+
$this->subject = $this->createPartialMock(Subject::class, ['createAnnotationReader']);
34+
}
35+
36+
public function testThrowsExceptionClassNotFound()
37+
{
38+
$requestedName = 'TestRepository';
39+
$this->expectException(RuntimeException::class);
40+
$this->expectExceptionMessage(RuntimeException::classNotFound($requestedName)->getMessage());
41+
42+
$this->subject->__invoke($this->container, $requestedName);
43+
}
44+
45+
public function testThrowsExceptionClassNotExtendsEntityRepository()
46+
{
47+
$requestedName = 'TestRepository';
48+
49+
$this->getMockBuilder($requestedName)->getMock();
50+
51+
$this->expectException(RuntimeException::class);
52+
$this->expectExceptionMessage(RuntimeException::doesNotExtend(EntityRepository::class)->getMessage());
53+
$this->subject->__invoke($this->container, $requestedName);
54+
}
55+
56+
public function testCreateObjectThrowsExceptionAnnotationNotFound()
57+
{
58+
$requestedName = TestRepository::class;
59+
60+
$this->annotationReader->method('getClassAnnotation')->willReturn(null);
61+
62+
$this->subject
63+
->method('createAnnotationReader')
64+
->willReturn($this->annotationReader);
65+
66+
$this->expectException(RuntimeException::class);
67+
$this->expectExceptionMessage(RuntimeException::annotationNotFound(
68+
Entity::class,
69+
$requestedName,
70+
get_class($this->subject)
71+
)->getMessage());
72+
73+
$this->subject->__invoke($this->container, $requestedName);
74+
}
75+
76+
public function testCreateObjectReturnsEntityRepository()
77+
{
78+
$requestedName = TestRepository::class;
79+
$annotation = new Entity('test');
80+
$entityManager = $this->createMock(EntityManagerInterface::class);
81+
$repository = $this->createMock(TestRepository::class);
82+
83+
$entityManager->method('getRepository')->willReturn($repository);
84+
85+
$this->annotationReader->method('getClassAnnotation')->willReturn($annotation);
86+
87+
$this->container->method('get')
88+
->with(EntityManagerInterface::class)
89+
->willReturn($entityManager);
90+
91+
$this->subject
92+
->method('createAnnotationReader')
93+
->willReturn($this->annotationReader);
94+
95+
$object = $this->subject->__invoke($this->container, $requestedName);
96+
97+
$this->assertInstanceOf(EntityRepository::class, $object);
98+
}
99+
}

0 commit comments

Comments
 (0)