diff --git a/src/Metadata/Driver/YamlDriver.php b/src/Metadata/Driver/YamlDriver.php index 6b4c11af0..aab7060f4 100644 --- a/src/Metadata/Driver/YamlDriver.php +++ b/src/Metadata/Driver/YamlDriver.php @@ -15,12 +15,16 @@ use JMS\Serializer\Type\Parser; use JMS\Serializer\Type\ParserInterface; use Metadata\ClassMetadata as BaseClassMetadata; -use Metadata\Driver\AbstractFileDriver; +use Metadata\Driver\AdvancedDriverInterface; +use Metadata\Driver\AdvancedFileLocatorInterface; +use Metadata\Driver\FileLocator; use Metadata\Driver\FileLocatorInterface; use Metadata\MethodMetadata; +use ReflectionClass; +use RuntimeException; use Symfony\Component\Yaml\Yaml; -class YamlDriver extends AbstractFileDriver +class YamlDriver implements AdvancedDriverInterface { use ExpressionMetadataTrait; @@ -32,27 +36,73 @@ class YamlDriver extends AbstractFileDriver * @var PropertyNamingStrategyInterface */ private $namingStrategy; + /** + * @var FileLocatorInterface|FileLocator + */ + private $locator; public function __construct(FileLocatorInterface $locator, PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null) { - parent::__construct($locator); + $this->locator = $locator; $this->typeParser = $typeParser ?? new Parser(); $this->namingStrategy = $namingStrategy; $this->expressionEvaluator = $expressionEvaluator; } - protected function loadMetadataFromFile(\ReflectionClass $class, string $file): ?BaseClassMetadata + public function loadMetadataForClass(ReflectionClass $class): ?BaseClassMetadata + { + $path = null; + foreach ($this->getExtensions() as $extension) { + $path = $this->locator->findFileForClass($class, $extension); + if ($path !== null) { + break; + } + } + + if (null === $path) { + return null; + } + + return $this->loadMetadataFromFile($class, $path); + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames(): array + { + if (!$this->locator instanceof AdvancedFileLocatorInterface) { + throw new RuntimeException( + sprintf( + 'Locator "%s" must be an instance of "AdvancedFileLocatorInterface".', get_class($this->locator) + ) + ); + } + + $classes = []; + foreach ($this->getExtensions() as $extension) { + foreach ($this->locator->findAllClasses($extension) as $class) { + $classes[$class] = $class; + } + } + + return array_values($classes); + } + + protected function loadMetadataFromFile(ReflectionClass $class, string $file): ?BaseClassMetadata { $config = Yaml::parse(file_get_contents($file)); if (!isset($config[$name = $class->name])) { - throw new InvalidMetadataException(sprintf('Expected metadata for class %s to be defined in %s.', $class->name, $file)); + throw new InvalidMetadataException( + sprintf('Expected metadata for class %s to be defined in %s.', $class->name, $file) + ); } $config = $config[$name]; $metadata = new ClassMetadata($name); $metadata->fileResources[] = $file; - $fileResource = $class->getFilename(); + $fileResource = $class->getFilename(); if (false !== $fileResource) { $metadata->fileResources[] = $fileResource; } @@ -75,7 +125,9 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $file): unset($propertySettings['exp']); } else { if (!$class->hasMethod($methodName)) { - throw new InvalidMetadataException('The method ' . $methodName . ' not found in class ' . $class->name); + throw new InvalidMetadataException( + 'The method ' . $methodName . ' not found in class ' . $class->name + ); } $virtualPropertyMetadata = new VirtualPropertyMetadata($name, $methodName); } @@ -285,6 +337,19 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $file): return $metadata; } + /** + * @return string[] + */ + protected function getExtensions(): array + { + return array_unique([$this->getExtension(), 'yaml', 'yml']); + } + + /** + * @return string + * + * @deprecated use getExtensions instead. + */ protected function getExtension(): string { return 'yml'; @@ -326,11 +391,15 @@ private function addClassProperties(ClassMetadata $metadata, array $config): voi throw new InvalidMetadataException('The "field_name" attribute must be set for discriminators.'); } - if (!isset($config['discriminator']['map']) || !\is_array($config['discriminator']['map'])) { - throw new InvalidMetadataException('The "map" attribute must be set, and be an array for discriminators.'); + if (!isset($config['discriminator']['map']) || !is_array($config['discriminator']['map'])) { + throw new InvalidMetadataException( + 'The "map" attribute must be set, and be an array for discriminators.' + ); } $groups = $config['discriminator']['groups'] ?? []; - $metadata->setDiscriminator($config['discriminator']['field_name'], $config['discriminator']['map'], $groups); + $metadata->setDiscriminator( + $config['discriminator']['field_name'], $config['discriminator']['map'], $groups + ); if (isset($config['discriminator']['xml_attribute'])) { $metadata->xmlDiscriminatorAttribute = (bool) $config['discriminator']['xml_attribute']; @@ -350,18 +419,25 @@ private function addClassProperties(ClassMetadata $metadata, array $config): voi /** * @param string|string[] $config */ - private function getCallbackMetadata(\ReflectionClass $class, $config): array + private function getCallbackMetadata(ReflectionClass $class, $config): array { - if (\is_string($config)) { + if (is_string($config)) { $config = [$config]; - } elseif (!\is_array($config)) { - throw new InvalidMetadataException(sprintf('callback methods expects a string, or an array of strings that represent method names, but got %s.', json_encode($config['pre_serialize']))); + } elseif (!is_array($config)) { + throw new InvalidMetadataException( + sprintf( + 'callback methods expects a string, or an array of strings that represent method names, but got %s.', + json_encode($config['pre_serialize']) + ) + ); } $methods = []; foreach ($config as $name) { if (!$class->hasMethod($name)) { - throw new InvalidMetadataException(sprintf('The method %s does not exist in class %s.', $name, $class->name)); + throw new InvalidMetadataException( + sprintf('The method %s does not exist in class %s.', $name, $class->name) + ); } $methods[] = new MethodMetadata($class->name, $name); diff --git a/tests/Metadata/Driver/YamlDriverTest.php b/tests/Metadata/Driver/YamlDriverTest.php index fa8d6cd6f..f39952c7c 100644 --- a/tests/Metadata/Driver/YamlDriverTest.php +++ b/tests/Metadata/Driver/YamlDriverTest.php @@ -8,26 +8,28 @@ use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; use Metadata\Driver\FileLocator; +use JMS\Serializer\Tests\Fixtures\Person; +use JMS\Serializer\Tests\Fixtures\BlogPost; class YamlDriverTest extends BaseDriverTest { - public function testAccessorOrderIsInferred() + public function testAccessorOrderIsInferred(): void { - $m = $this->getDriverForSubDir('accessor_inferred')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Person')); + $m = $this->getDriverForSubDir('accessor_inferred')->loadMetadataForClass(new \ReflectionClass(Person::class)); self::assertEquals(['age', 'name'], array_keys($m->propertyMetadata)); } - public function testShortExposeSyntax() + public function testShortExposeSyntax(): void { - $m = $this->getDriverForSubDir('short_expose')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Person')); + $m = $this->getDriverForSubDir('short_expose')->loadMetadataForClass(new \ReflectionClass(Person::class)); self::assertArrayHasKey('name', $m->propertyMetadata); self::assertArrayNotHasKey('age', $m->propertyMetadata); } - public function testBlogPost() + public function testBlogPost(): void { - $m = $this->getDriverForSubDir('exclude_all')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\BlogPost')); + $m = $this->getDriverForSubDir('exclude_all')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayHasKey('title', $m->propertyMetadata); @@ -37,9 +39,9 @@ public function testBlogPost() } } - public function testBlogPostExcludeNoneStrategy() + public function testBlogPostExcludeNoneStrategy(): void { - $m = $this->getDriverForSubDir('exclude_none')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\BlogPost')); + $m = $this->getDriverForSubDir('exclude_none')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayNotHasKey('title', $m->propertyMetadata); @@ -49,9 +51,9 @@ public function testBlogPostExcludeNoneStrategy() } } - public function testBlogPostCaseInsensitive() + public function testBlogPostCaseInsensitive(): void { - $m = $this->getDriverForSubDir('case')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\BlogPost')); + $m = $this->getDriverForSubDir('case')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); $p = new PropertyMetadata($m->name, 'title'); $p->serializedName = 'title'; @@ -59,9 +61,9 @@ public function testBlogPostCaseInsensitive() self::assertEquals($p, $m->propertyMetadata['title']); } - public function testBlogPostAccessor() + public function testBlogPostAccessor(): void { - $m = $this->getDriverForSubDir('accessor')->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\BlogPost')); + $m = $this->getDriverForSubDir('accessor')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayHasKey('title', $m->propertyMetadata); @@ -72,12 +74,45 @@ public function testBlogPostAccessor() self::assertEquals($p, $m->propertyMetadata['title']); } - private function getDriverForSubDir($subDir = null) + /** + * @expectedException \JMS\Serializer\Exception\InvalidMetadataException + */ + public function testInvalidMetadataFileCausesException(): void { - return new YamlDriver(new FileLocator([ + $this->getDriverForSubDir('invalid_metadata')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + } + + public function testLoadingYamlFileWithLongExtension(): void + { + $m = $this->getDriverForSubDir('multiple_types')->loadMetadataForClass(new \ReflectionClass(Person::class)); + + self::assertArrayHasKey('name', $m->propertyMetadata); + } + + public function testLoadingMultipleMetadataExtensions(): void + { + $classNames = $this->getDriverForSubDir('multiple_types', false)->getAllClassNames(); + + self::assertEquals( + [ + BlogPost::class, + Person::class + ], + $classNames + ); + } + + private function getDriverForSubDir($subDir = null, bool $addUnderscoreDir = true): YamlDriver + { + $dirs = [ 'JMS\Serializer\Tests\Fixtures' => __DIR__ . '/yml' . ($subDir ? '/' . $subDir : ''), - '' => __DIR__ . '/yml/_' . ($subDir ? '/' . $subDir : ''), - ]), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); + ]; + + if ($addUnderscoreDir) { + $dirs[''] = __DIR__ . '/yml/_' . ($subDir ? '/' . $subDir : ''); + } + + return new YamlDriver(new FileLocator($dirs), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); } protected function getDriver() diff --git a/tests/Metadata/Driver/yml/invalid_metadata/BlogPost.yml b/tests/Metadata/Driver/yml/invalid_metadata/BlogPost.yml new file mode 100644 index 000000000..933e03fc3 --- /dev/null +++ b/tests/Metadata/Driver/yml/invalid_metadata/BlogPost.yml @@ -0,0 +1,6 @@ +JMS\Serializer\Tests\Fixtures\Person: + custom_accessor_order: ["age", "name"] + + properties: + age: ~ + name: ~ diff --git a/tests/Metadata/Driver/yml/multiple_types/BlogPost.yml b/tests/Metadata/Driver/yml/multiple_types/BlogPost.yml new file mode 100644 index 000000000..7067cf822 --- /dev/null +++ b/tests/Metadata/Driver/yml/multiple_types/BlogPost.yml @@ -0,0 +1,7 @@ +JMS\Serializer\Tests\Fixtures\BlogPost: + xml_root_name: blog-post + exclusion_policy: NONE + properties: + title: + type: string + exclude: true diff --git a/tests/Metadata/Driver/yml/multiple_types/Person.yaml b/tests/Metadata/Driver/yml/multiple_types/Person.yaml new file mode 100644 index 000000000..4ab55bea9 --- /dev/null +++ b/tests/Metadata/Driver/yml/multiple_types/Person.yaml @@ -0,0 +1,4 @@ +JMS\Serializer\Tests\Fixtures\Person: + exclusion_policy: ALL + properties: + name: ~