1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Tests\Serializer\Doctrine;
6
7use Doctrine\Common\Annotations\AnnotationReader;
8use Doctrine\Common\Annotations\Reader;
9use Doctrine\Common\Persistence\AbstractManagerRegistry;
10use Doctrine\Common\Persistence\ManagerRegistry;
11use Doctrine\DBAL\Connection;
12use Doctrine\DBAL\DriverManager;
13use Doctrine\DBAL\Types\Type;
14use Doctrine\ORM\Configuration;
15use Doctrine\ORM\EntityManager;
16use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
17use Doctrine\ORM\ORMException;
18use Doctrine\ORM\Tools\SchemaTool;
19use Doctrine\ORM\UnitOfWork;
20use JMS\Serializer\Builder\CallbackDriverFactory;
21use JMS\Serializer\Builder\DefaultDriverFactory;
22use JMS\Serializer\Construction\DoctrineObjectConstructor;
23use JMS\Serializer\Construction\ObjectConstructorInterface;
24use JMS\Serializer\Construction\UnserializeObjectConstructor;
25use JMS\Serializer\DeserializationContext;
26use JMS\Serializer\Metadata\ClassMetadata;
27use JMS\Serializer\Metadata\Driver\DoctrineTypeDriver;
28use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy;
29use JMS\Serializer\Serializer;
30use JMS\Serializer\SerializerBuilder;
31use JMS\Serializer\SerializerInterface;
32use JMS\Serializer\Tests\Fixtures\Doctrine\Author;
33use JMS\Serializer\Tests\Fixtures\Doctrine\IdentityFields\Server;
34use JMS\Serializer\Tests\Fixtures\DoctrinePHPCR\Author as DoctrinePHPCRAuthor;
35use JMS\Serializer\Visitor\DeserializationVisitorInterface;
36use PHPUnit\Framework\TestCase;
37
38class ObjectConstructorTest extends TestCase
39{
40    /** @var ManagerRegistry */
41    private $registry;
42
43    /** @var Serializer */
44    private $serializer;
45
46    /** @var DeserializationVisitorInterface */
47    private $visitor;
48
49    /** @var DeserializationContext */
50    private $context;
51
52    public function testFindEntity()
53    {
54        $em = $this->registry->getManager();
55
56        $author = new Author('John', 5);
57        $em->persist($author);
58        $em->flush();
59        $em->clear();
60
61        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
62
63        $type = ['name' => Author::class, 'params' => []];
64        $class = new ClassMetadata(Author::class);
65
66        $constructor = new DoctrineObjectConstructor($this->registry, $fallback);
67        $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
68
69        self::assertEquals($author, $authorFetched);
70    }
71
72    public function testFindManagedEntity()
73    {
74        $em = $this->registry->getManager();
75
76        $author = new Author('John', 5);
77        $em->persist($author);
78        $em->flush();
79
80        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
81
82        $type = ['name' => Author::class, 'params' => []];
83        $class = new ClassMetadata(Author::class);
84
85        $constructor = new DoctrineObjectConstructor($this->registry, $fallback);
86        $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
87
88        self::assertSame($author, $authorFetched);
89    }
90
91    public function testMissingAuthor()
92    {
93        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
94
95        $type = ['name' => Author::class, 'params' => []];
96        $class = new ClassMetadata(Author::class);
97
98        $constructor = new DoctrineObjectConstructor($this->registry, $fallback);
99        $author = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
100        self::assertNull($author);
101    }
102
103    public function testMissingAuthorFallback()
104    {
105        $author = new Author('John');
106
107        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
108        $fallback->expects($this->once())->method('construct')->willReturn($author);
109
110        $type = ['name' => Author::class, 'params' => []];
111        $class = new ClassMetadata(Author::class);
112
113        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK);
114        $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
115        self::assertSame($author, $authorFetched);
116    }
117
118    public function testMissingNotManaged()
119    {
120        $author = new DoctrinePHPCRAuthor('foo');
121
122        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
123        $fallback->expects($this->once())->method('construct')->willReturn($author);
124
125        $type = ['name' => Author::class, 'params' => []];
126        $class = new ClassMetadata(Author::class);
127
128        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK);
129        $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
130        self::assertSame($author, $authorFetched);
131    }
132
133    public function testReference()
134    {
135        $em = $this->registry->getManager();
136
137        $author = new Author('John', 5);
138        $em->persist($author);
139        $em->flush();
140
141        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
142
143        $type = ['name' => Author::class, 'params' => []];
144        $class = new ClassMetadata(Author::class);
145
146        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK);
147        $authorFetched = $constructor->construct($this->visitor, $class, 5, $type, $this->context);
148        self::assertSame($author, $authorFetched);
149    }
150
151    /**
152     * @expectedException \JMS\Serializer\Exception\ObjectConstructionException
153     */
154    public function testMissingAuthorException()
155    {
156        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
157
158        $type = ['name' => Author::class, 'params' => []];
159        $class = new ClassMetadata(Author::class);
160
161        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_EXCEPTION);
162        $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
163    }
164
165    /**
166     * @expectedException \JMS\Serializer\Exception\InvalidArgumentException
167     */
168    public function testInvalidArg()
169    {
170        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
171
172        $type = ['name' => Author::class, 'params' => []];
173        $class = new ClassMetadata(Author::class);
174
175        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, 'foo');
176        $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context);
177    }
178
179    public function testMissingData()
180    {
181        $author = new Author('John');
182
183        $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock();
184        $fallback->expects($this->once())->method('construct')->willReturn($author);
185
186        $type = ['name' => Author::class, 'params' => []];
187        $class = new ClassMetadata(Author::class);
188
189        $constructor = new DoctrineObjectConstructor($this->registry, $fallback, 'foo');
190        $authorFetched = $constructor->construct($this->visitor, $class, ['foo' => 5], $type, $this->context);
191        self::assertSame($author, $authorFetched);
192    }
193
194    public function testNamingForIdentifierColumnIsConsidered()
195    {
196        $serializer = $this->createSerializerWithDoctrineObjectConstructor();
197
198        /** @var EntityManager $em */
199        $em = $this->registry->getManager();
200        $server = new Server('Linux', '127.0.0.1', 'home');
201        $em->persist($server);
202        $em->flush();
203        $em->clear();
204
205        $jsonData = '{"ip_address":"127.0.0.1", "server_id_extracted":"home", "name":"Windows"}';
206        /** @var Server $serverDeserialized */
207        $serverDeserialized = $serializer->deserialize($jsonData, Server::class, 'json');
208
209        static::assertSame(
210            $em->getUnitOfWork()->getEntityState($serverDeserialized),
211            UnitOfWork::STATE_MANAGED
212        );
213    }
214
215    protected function setUp()
216    {
217        $this->visitor = $this->getMockBuilder(DeserializationVisitorInterface::class)->getMock();
218        $this->context = $this->getMockBuilder('JMS\Serializer\DeserializationContext')->getMock();
219
220        $connection = $this->createConnection();
221        $entityManager = $this->createEntityManager($connection);
222
223        $this->registry = $registry = new SimpleBaseManagerRegistry(
224            static function ($id) use ($connection, $entityManager) {
225                switch ($id) {
226                    case 'default_connection':
227                        return $connection;
228
229                    case 'default_manager':
230                        return $entityManager;
231
232                    default:
233                        throw new \RuntimeException(sprintf('Unknown service id "%s".', $id));
234                }
235            }
236        );
237
238        $this->serializer = SerializerBuilder::create()
239            ->setMetadataDriverFactory(new CallbackDriverFactory(
240                static function (array $metadataDirs, Reader $annotationReader) use ($registry) {
241                    $defaultFactory = new DefaultDriverFactory(new IdenticalPropertyNamingStrategy());
242
243                    return new DoctrineTypeDriver($defaultFactory->createDriver($metadataDirs, $annotationReader), $registry);
244                }
245            ))
246            ->build();
247
248        $this->prepareDatabase();
249    }
250
251    private function prepareDatabase()
252    {
253        /** @var EntityManager $em */
254        $em = $this->registry->getManager();
255
256        $tool = new SchemaTool($em);
257        $tool->createSchema($em->getMetadataFactory()->getAllMetadata());
258    }
259
260    private function createConnection()
261    {
262        return DriverManager::getConnection([
263            'driver' => 'pdo_sqlite',
264            'memory' => true,
265        ]);
266    }
267
268    private function createEntityManager(Connection $con)
269    {
270        $cfg = new Configuration();
271        $cfg->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [
272            __DIR__ . '/../../Fixtures/Doctrine',
273        ]));
274        $cfg->setAutoGenerateProxyClasses(true);
275        $cfg->setProxyNamespace('JMS\Serializer\DoctrineProxy');
276        $cfg->setProxyDir(sys_get_temp_dir() . '/serializer-test-proxies');
277
278        return EntityManager::create($con, $cfg);
279    }
280
281    /**
282     * @return SerializerInterface
283     */
284    private function createSerializerWithDoctrineObjectConstructor()
285    {
286        return SerializerBuilder::create()
287            ->setObjectConstructor(
288                new DoctrineObjectConstructor(
289                    $this->registry,
290                    new UnserializeObjectConstructor(),
291                    DoctrineObjectConstructor::ON_MISSING_FALLBACK
292                )
293            )
294            ->addDefaultHandlers()
295            ->build();
296    }
297}
298
299Type::addType('Author', 'Doctrine\DBAL\Types\StringType');
300Type::addType('some_custom_type', 'Doctrine\DBAL\Types\StringType');
301
302class SimpleBaseManagerRegistry extends AbstractManagerRegistry
303{
304    private $services = [];
305    private $serviceCreator;
306
307    public function __construct($serviceCreator, $name = 'anonymous', array $connections = ['default' => 'default_connection'], array $managers = ['default' => 'default_manager'], $defaultConnection = null, $defaultManager = null, $proxyInterface = 'Doctrine\Common\Persistence\Proxy')
308    {
309        if (null === $defaultConnection) {
310            $defaultConnection = key($connections);
311        }
312        if (null === $defaultManager) {
313            $defaultManager = key($managers);
314        }
315
316        parent::__construct($name, $connections, $managers, $defaultConnection, $defaultManager, $proxyInterface);
317
318        if (!is_callable($serviceCreator)) {
319            throw new \InvalidArgumentException('$serviceCreator must be a valid callable.');
320        }
321        $this->serviceCreator = $serviceCreator;
322    }
323
324    public function getService($name)
325    {
326        if (isset($this->services[$name])) {
327            return $this->services[$name];
328        }
329
330        return $this->services[$name] = call_user_func($this->serviceCreator, $name);
331    }
332
333    public function resetService($name)
334    {
335        unset($this->services[$name]);
336    }
337
338    public function getAliasNamespace($alias)
339    {
340        foreach (array_keys($this->getManagers()) as $name) {
341            $manager = $this->getManager($name);
342
343            if ($manager instanceof EntityManager) {
344                try {
345                    return $manager->getConfiguration()->getEntityNamespace($alias);
346                } catch (ORMException $ex) {
347                    // Probably mapped by another entity manager, or invalid, just ignore this here.
348                }
349            } else {
350                throw new \LogicException(sprintf('Unsupported manager type "%s".', get_class($manager)));
351            }
352        }
353
354        throw new \RuntimeException(sprintf('The namespace alias "%s" is not known to any manager.', $alias));
355    }
356}
357