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