b2pweb /
bdf-serializer
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Bdf\Serializer\Metadata\Driver; |
||
| 4 | |||
| 5 | use Bdf\Serializer\Metadata\Builder\ClassMetadataBuilder; |
||
| 6 | use Bdf\Serializer\Metadata\ClassMetadata; |
||
| 7 | use Bdf\Serializer\Type\Type; |
||
| 8 | use phpDocumentor\Reflection\DocBlock; |
||
| 9 | use phpDocumentor\Reflection\DocBlock\Tag; |
||
| 10 | use phpDocumentor\Reflection\DocBlockFactory; |
||
| 11 | use phpDocumentor\Reflection\Types\ContextFactory; |
||
| 12 | use ReflectionClass; |
||
| 13 | use ReflectionProperty; |
||
| 14 | |||
| 15 | /** |
||
| 16 | * AnnotationsDriver |
||
| 17 | * |
||
| 18 | * based on doctrine annotations |
||
| 19 | * |
||
| 20 | * @author Seb |
||
| 21 | */ |
||
| 22 | class AnnotationsDriver implements DriverInterface |
||
| 23 | { |
||
| 24 | /** |
||
| 25 | * @var DocBlockFactory |
||
| 26 | */ |
||
| 27 | private $docBlockFactory; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * @var ContextFactory |
||
| 31 | */ |
||
| 32 | private $contextFactory; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * AnnotationsDriver constructor. |
||
| 36 | */ |
||
| 37 | 178 | public function __construct() |
|
| 38 | { |
||
| 39 | 178 | $this->docBlockFactory = DocBlockFactory::createInstance(); |
|
| 40 | 178 | $this->contextFactory = new ContextFactory(); |
|
| 41 | } |
||
| 42 | |||
| 43 | /** |
||
| 44 | * {@inheritdoc} |
||
| 45 | */ |
||
| 46 | 76 | public function getMetadataForClass(ReflectionClass $class): ?ClassMetadata |
|
| 47 | { |
||
| 48 | 76 | if ($class->isInterface() || $class->isAbstract()) { |
|
| 49 | return null; |
||
| 50 | } |
||
| 51 | |||
| 52 | 76 | $annotations = []; |
|
| 53 | 76 | $reflection = $class; |
|
| 54 | |||
| 55 | // Get all properties annotations from the hierarchy |
||
| 56 | do { |
||
| 57 | 76 | foreach ($this->getClassProperties($reflection) as $property) { |
|
| 58 | // PHP serialize behavior: we skip the static properties. |
||
| 59 | 74 | if ($property->isStatic()) { |
|
| 60 | 2 | continue; |
|
| 61 | } |
||
| 62 | |||
| 63 | 74 | $annotation = $this->getPropertyAnnotations($property); |
|
| 64 | |||
| 65 | 74 | if (isset($annotation['SerializeIgnore'])) { |
|
| 66 | 2 | continue; |
|
| 67 | } |
||
| 68 | |||
| 69 | 74 | if (isset($annotations[$property->name])) { |
|
| 70 | 2 | $annotations[$property->name] = array_merge($annotation, $annotations[$property->name]); |
|
| 71 | } else { |
||
| 72 | 74 | $annotations[$property->name] = $annotation; |
|
| 73 | } |
||
| 74 | } |
||
| 75 | |||
| 76 | 76 | $reflection = $reflection->getParentClass(); |
|
| 77 | 76 | } while ($reflection); |
|
| 78 | |||
| 79 | // Parse annotations |
||
| 80 | 76 | $builder = new ClassMetadataBuilder($class); |
|
| 81 | |||
| 82 | 76 | if ($class->hasMethod('__wakeup')) { |
|
| 83 | 4 | $builder->postDenormalization('__wakeup'); |
|
| 84 | } |
||
| 85 | |||
| 86 | 76 | foreach ($annotations as $name => $annotation) { |
|
| 87 | 74 | $property = $builder->add($name, isset($annotation['type']) ? $annotation['type'] : Type::MIXED); |
|
| 88 | |||
| 89 | 74 | if (isset($annotation['since'])) { |
|
| 90 | 2 | $property->since($annotation['since']); |
|
| 91 | } |
||
| 92 | |||
| 93 | 74 | if (isset($annotation['until'])) { |
|
| 94 | 2 | $property->until($annotation['until']); |
|
| 95 | } |
||
| 96 | } |
||
| 97 | |||
| 98 | 76 | return $builder->build(); |
|
| 99 | } |
||
| 100 | |||
| 101 | /** |
||
| 102 | * Gets the class properties |
||
| 103 | * |
||
| 104 | * @param ReflectionClass $reflection |
||
| 105 | * |
||
| 106 | * @return ReflectionProperty[] |
||
| 107 | */ |
||
| 108 | 76 | private function getClassProperties(ReflectionClass $reflection): array |
|
| 109 | { |
||
| 110 | 76 | if (!$reflection->hasMethod('__sleep')) { |
|
| 111 | // The class has no magic method __sleep, we return all the properties. |
||
| 112 | 72 | return $reflection->getProperties(); |
|
| 113 | } |
||
| 114 | |||
| 115 | 4 | $properties = []; |
|
| 116 | 4 | $instance = $reflection->newInstanceWithoutConstructor(); |
|
| 117 | |||
| 118 | 4 | foreach ($reflection->getMethod('__sleep')->invoke($instance) as $name) { |
|
| 119 | 4 | $properties[] = $reflection->getProperty($name); |
|
| 120 | } |
||
| 121 | |||
| 122 | 4 | return $properties; |
|
| 123 | } |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Get annotations from the property |
||
| 127 | * |
||
| 128 | * @param ReflectionProperty $property |
||
| 129 | * |
||
| 130 | * @return array |
||
| 131 | */ |
||
| 132 | 74 | private function getPropertyAnnotations(ReflectionProperty $property): array |
|
| 133 | { |
||
| 134 | try { |
||
| 135 | 74 | $tags = $this->docBlockFactory->create($property, $this->contextFactory->createFromReflector($property))->getTags(); |
|
| 136 | 62 | } catch (\InvalidArgumentException $e) { |
|
| 137 | 62 | $tags = []; |
|
| 138 | } |
||
| 139 | |||
| 140 | 74 | $annotations = []; |
|
| 141 | |||
| 142 | // Tags mapping |
||
| 143 | 74 | foreach ($tags as $tag) { |
|
| 144 | 24 | list($option, $value) = $this->createSerializationTag($tag, $property); |
|
| 145 | |||
| 146 | 24 | if ($option !== null && !isset($annotations[$option])) { |
|
| 147 | 24 | $annotations[$option] = $value; |
|
| 148 | } |
||
| 149 | } |
||
| 150 | |||
| 151 | // Adding php type if no precision has been added with annotation |
||
| 152 | 74 | if (PHP_VERSION_ID >= 70400 && $property->hasType() && !isset($annotations['type'])) { |
|
| 153 | /** @psalm-suppress UndefinedMethod */ |
||
| 154 | 24 | $annotations['type'] = $this->findType($property->getType()->getName(), $property); |
|
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 155 | } |
||
| 156 | |||
| 157 | 74 | return $annotations; |
|
| 158 | } |
||
| 159 | |||
| 160 | /** |
||
| 161 | * Create the serialization info |
||
| 162 | * |
||
| 163 | * @param Tag $tag |
||
| 164 | * @param ReflectionProperty $property |
||
| 165 | * |
||
| 166 | * @return array |
||
| 167 | */ |
||
| 168 | 24 | private function createSerializationTag($tag, $property): array |
|
| 169 | { |
||
| 170 | 24 | switch ($tag->getName()) { |
|
| 171 | 24 | case 'var': |
|
| 172 | 24 | if ($tag instanceof DocBlock\Tags\InvalidTag) { |
|
| 173 | 4 | return ['type', $this->findType((string) $tag, $property)]; |
|
| 174 | } |
||
| 175 | |||
| 176 | /** @var DocBlock\Tags\Var_ $tag */ |
||
| 177 | 24 | return ['type', $this->findType((string)$tag->getType(), $property)]; |
|
| 178 | |||
| 179 | 4 | case 'since': |
|
| 180 | /** @var DocBlock\Tags\Since $tag */ |
||
| 181 | 2 | return ['since', (string)$tag->getVersion()]; |
|
| 182 | |||
| 183 | 4 | case 'until': |
|
| 184 | /** @var DocBlock\Tags\Generic $tag */ |
||
| 185 | 2 | return ['until', (string)$tag->getDescription()]; |
|
| 186 | |||
| 187 | 2 | case 'SerializeIgnore': |
|
| 188 | 2 | return ['SerializeIgnore', true]; |
|
| 189 | } |
||
| 190 | |||
| 191 | return [null, null]; |
||
| 192 | } |
||
| 193 | |||
| 194 | /** |
||
| 195 | * Filter the var tag |
||
| 196 | * |
||
| 197 | * @param string $var |
||
| 198 | * @param ReflectionProperty $property |
||
| 199 | * |
||
| 200 | * @return string |
||
| 201 | */ |
||
| 202 | 44 | private function findType($var, $property): ?string |
|
| 203 | { |
||
| 204 | // Clear psalm structure notation and generics |
||
| 205 | 44 | $var = preg_replace('/(.*)\{.*\}/u', '$1', $var); |
|
| 206 | 44 | $var = preg_replace('/(.*)<.*>/u', '$1', $var); |
|
| 207 | |||
| 208 | // All known alias from phpdoc that should be mapped to a serializer type |
||
| 209 | 44 | $alias = [ |
|
| 210 | 44 | 'bool' => Type::BOOLEAN, |
|
| 211 | 44 | 'false' => Type::BOOLEAN, |
|
| 212 | 44 | 'true' => Type::BOOLEAN, |
|
| 213 | 44 | 'int' => Type::INTEGER, |
|
| 214 | 44 | 'void' => Type::TNULL, |
|
| 215 | 44 | 'scalar' => Type::STRING, |
|
| 216 | 44 | 'iterable' => Type::TARRAY, |
|
| 217 | 44 | 'object' => \stdClass::class, |
|
| 218 | 44 | 'callback' => 'callable', |
|
| 219 | 44 | 'self' => $property->class, |
|
| 220 | 44 | '$this' => $property->class, |
|
| 221 | 44 | 'static' => $property->class, |
|
| 222 | 44 | ]; |
|
| 223 | |||
| 224 | 44 | if (strpos($var, '|') === false) { |
|
| 225 | 42 | $var = ltrim($var, '\\'); |
|
| 226 | |||
| 227 | 42 | return isset($alias[$var]) ? $alias[$var] : $var; |
|
| 228 | } |
||
| 229 | |||
| 230 | 8 | foreach (explode('|', $var) as $candidate) { |
|
| 231 | 8 | $candidate = ltrim($candidate, '\\'); |
|
| 232 | |||
| 233 | 8 | if (isset($alias[$candidate])) { |
|
| 234 | 2 | $candidate = $alias[$candidate]; |
|
| 235 | } |
||
| 236 | |||
| 237 | 8 | if ($candidate !== '' && $candidate !== Type::TNULL) { |
|
| 238 | 8 | return $candidate; |
|
| 239 | } |
||
| 240 | } |
||
| 241 | |||
| 242 | // We let here the getMetadataForClass add the default type |
||
| 243 | return null; |
||
| 244 | } |
||
| 245 | } |
||
| 246 |