1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Metadata\Driver;
6
7use Doctrine\Common\Persistence\ManagerRegistry;
8use Doctrine\Common\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata;
9use JMS\Serializer\Metadata\ClassMetadata;
10use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
11use JMS\Serializer\Metadata\PropertyMetadata;
12use JMS\Serializer\Metadata\StaticPropertyMetadata;
13use JMS\Serializer\Metadata\VirtualPropertyMetadata;
14use JMS\Serializer\Type\Parser;
15use JMS\Serializer\Type\ParserInterface;
16use Metadata\ClassMetadata as BaseClassMetadata;
17use Metadata\Driver\DriverInterface;
18
19/**
20 * This class decorates any other driver. If the inner driver does not provide a
21 * a property type, the decorator will guess based on Doctrine 2 metadata.
22 */
23abstract class AbstractDoctrineTypeDriver implements DriverInterface
24{
25    /**
26     * Map of doctrine 2 field types to JMS\Serializer types
27     *
28     * @var array
29     */
30    protected $fieldMapping = [
31        'string' => 'string',
32        'text' => 'string',
33        'blob' => 'string',
34        'guid' => 'string',
35
36        'integer' => 'integer',
37        'smallint' => 'integer',
38        'bigint' => 'integer',
39
40        'datetime' => 'DateTime',
41        'datetimetz' => 'DateTime',
42        'time' => 'DateTime',
43        'date' => 'DateTime',
44
45        'float' => 'float',
46        'decimal' => 'float',
47
48        'boolean' => 'boolean',
49
50        'array' => 'array',
51        'json_array' => 'array',
52        'simple_array' => 'array<string>',
53    ];
54    /**
55     * @var DriverInterface
56     */
57    protected $delegate;
58    /**
59     * @var ManagerRegistry
60     */
61    protected $registry;
62
63    /**
64     * @var ParserInterface
65     */
66    protected $typeParser;
67
68    public function __construct(DriverInterface $delegate, ManagerRegistry $registry, ?ParserInterface $typeParser = null)
69    {
70        $this->delegate = $delegate;
71        $this->registry = $registry;
72        $this->typeParser = $typeParser ?: new Parser();
73    }
74
75    public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadata
76    {
77        /** @var ClassMetadata $classMetadata */
78        $classMetadata = $this->delegate->loadMetadataForClass($class);
79
80        // Abort if the given class is not a mapped entity
81        if (!$doctrineMetadata = $this->tryLoadingDoctrineMetadata($class->name)) {
82            return $classMetadata;
83        }
84
85        $this->setDiscriminator($doctrineMetadata, $classMetadata);
86
87        // We base our scan on the internal driver's property list so that we
88        // respect any internal white/blacklisting like in the AnnotationDriver
89        foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) {
90            /** @var $propertyMetadata PropertyMetadata */
91
92            // If the inner driver provides a type, don't guess anymore.
93            if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) {
94                continue;
95            }
96
97            if ($this->hideProperty($doctrineMetadata, $propertyMetadata)) {
98                unset($classMetadata->propertyMetadata[$key]);
99            }
100
101            $this->setPropertyType($doctrineMetadata, $propertyMetadata);
102        }
103
104        return $classMetadata;
105    }
106
107    private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool
108    {
109        return $propertyMetadata instanceof VirtualPropertyMetadata
110            || $propertyMetadata instanceof StaticPropertyMetadata
111            || $propertyMetadata instanceof ExpressionPropertyMetadata;
112    }
113
114    protected function setDiscriminator(DoctrineClassMetadata $doctrineMetadata, ClassMetadata $classMetadata): void
115    {
116    }
117
118    protected function hideProperty(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): bool
119    {
120        return false;
121    }
122
123    protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void
124    {
125    }
126
127    protected function tryLoadingDoctrineMetadata(string $className): ?DoctrineClassMetadata
128    {
129        if (!$manager = $this->registry->getManagerForClass($className)) {
130            return null;
131        }
132
133        if ($manager->getMetadataFactory()->isTransient($className)) {
134            return null;
135        }
136
137        return $manager->getClassMetadata($className);
138    }
139
140    protected function normalizeFieldType(string $type): ?string
141    {
142        if (!isset($this->fieldMapping[$type])) {
143            return null;
144        }
145
146        return $this->fieldMapping[$type];
147    }
148}
149