1<?php
2
3declare(strict_types=1);
4
5namespace Metadata;
6
7use Metadata\Cache\CacheInterface;
8use Metadata\Driver\AdvancedDriverInterface;
9use Metadata\Driver\DriverInterface;
10
11class MetadataFactory implements AdvancedMetadataFactoryInterface
12{
13    /**
14     * @var DriverInterface
15     */
16    private $driver;
17
18    /**
19     * @var CacheInterface
20     */
21    private $cache;
22
23    /**
24     * @var ClassMetadata[]
25     */
26    private $loadedMetadata = [];
27
28    /**
29     * @var ClassMetadata[]
30     */
31    private $loadedClassMetadata = [];
32
33    /**
34     * @var null|string
35     */
36    private $hierarchyMetadataClass;
37
38    /**
39     * @var bool
40     */
41    private $includeInterfaces = false;
42
43    /**
44     * @var bool
45     */
46    private $debug = false;
47
48    public function __construct(DriverInterface $driver, ?string $hierarchyMetadataClass = 'Metadata\ClassHierarchyMetadata', bool $debug = false)
49    {
50        $this->driver = $driver;
51        $this->hierarchyMetadataClass = $hierarchyMetadataClass;
52        $this->debug = $debug;
53    }
54
55    public function setIncludeInterfaces(bool $include): void
56    {
57        $this->includeInterfaces = $include;
58    }
59
60    public function setCache(CacheInterface $cache): void
61    {
62        $this->cache = $cache;
63    }
64
65
66    /**
67     * {@inheritDoc}
68     */
69    public function getMetadataForClass(string $className)
70    {
71        if (isset($this->loadedMetadata[$className])) {
72            return $this->filterNullMetadata($this->loadedMetadata[$className]);
73        }
74
75        $metadata = null;
76        foreach ($this->getClassHierarchy($className) as $class) {
77            if (isset($this->loadedClassMetadata[$name = $class->getName()])) {
78                if (null !== $classMetadata = $this->filterNullMetadata($this->loadedClassMetadata[$name])) {
79                    $this->addClassMetadata($metadata, $classMetadata);
80                }
81                continue;
82            }
83
84            // check the cache
85            if (null !== $this->cache) {
86                if (($classMetadata = $this->cache->load($class->getName())) instanceof NullMetadata) {
87                    $this->loadedClassMetadata[$name] = $classMetadata;
88                    continue;
89                }
90
91                if (null !== $classMetadata) {
92                    if (!$classMetadata instanceof ClassMetadata) {
93                        throw new \LogicException(sprintf('The cache must return instances of ClassMetadata, but got %s.', var_export($classMetadata, true)));
94                    }
95
96                    if ($this->debug && !$classMetadata->isFresh()) {
97                        $this->cache->evict($classMetadata->name);
98                    } else {
99                        $this->loadedClassMetadata[$name] = $classMetadata;
100                        $this->addClassMetadata($metadata, $classMetadata);
101                        continue;
102                    }
103                }
104            }
105
106            // load from source
107            if (null !== $classMetadata = $this->driver->loadMetadataForClass($class)) {
108                $this->loadedClassMetadata[$name] = $classMetadata;
109                $this->addClassMetadata($metadata, $classMetadata);
110
111                if (null !== $this->cache) {
112                    $this->cache->put($classMetadata);
113                }
114
115                continue;
116            }
117
118            if (null !== $this->cache && !$this->debug) {
119                $this->cache->put(new NullMetadata($class->getName()));
120            }
121        }
122
123        if (null === $metadata) {
124            $metadata = new NullMetadata($className);
125        }
126
127        return $this->filterNullMetadata($this->loadedMetadata[$className] = $metadata);
128    }
129
130    /**
131     * {@inheritDoc}
132     */
133    public function getAllClassNames(): array
134    {
135        if (!$this->driver instanceof AdvancedDriverInterface) {
136            throw new \RuntimeException(
137                sprintf('Driver "%s" must be an instance of "AdvancedDriverInterface".', get_class($this->driver))
138            );
139        }
140
141        return $this->driver->getAllClassNames();
142    }
143
144    /**
145     * @param MergeableInterface|ClassHierarchyMetadata $metadata
146     */
147    private function addClassMetadata(&$metadata, ClassMetadata $toAdd): void
148    {
149        if ($toAdd instanceof MergeableInterface) {
150            if (null === $metadata) {
151                $metadata = clone $toAdd;
152            } else {
153                $metadata->merge($toAdd);
154            }
155        } else {
156            if (null === $metadata) {
157                $metadata = new $this->hierarchyMetadataClass();
158            }
159
160            $metadata->addClassMetadata($toAdd);
161        }
162    }
163
164    /**
165     * @return \ReflectionClass[]
166     */
167    private function getClassHierarchy(string $class): array
168    {
169        $classes = [];
170        $refl = new \ReflectionClass($class);
171
172        do {
173            $classes[] = $refl;
174            $refl = $refl->getParentClass();
175        } while (false !== $refl);
176
177        $classes = array_reverse($classes, false);
178
179        if (!$this->includeInterfaces) {
180            return $classes;
181        }
182
183        $addedInterfaces = [];
184        $newHierarchy = [];
185
186        foreach ($classes as $class) {
187            foreach ($class->getInterfaces() as $interface) {
188                if (isset($addedInterfaces[$interface->getName()])) {
189                    continue;
190                }
191                $addedInterfaces[$interface->getName()] = true;
192
193                $newHierarchy[] = $interface;
194            }
195
196            $newHierarchy[] = $class;
197        }
198
199        return $newHierarchy;
200    }
201
202    /**
203     * @param ClassMetadata|ClassHierarchyMetadata|MergeableInterface $metadata
204     * @return ClassMetadata|ClassHierarchyMetadata|MergeableInterface
205     */
206    private function filterNullMetadata($metadata = null)
207    {
208        return !$metadata instanceof NullMetadata ? $metadata : null;
209    }
210}
211