Skip to content

Commit

Permalink
Allow loading multiple YAML extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel.Batanov committed Apr 30, 2019
1 parent f958936 commit 51bbf35
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 31 deletions.
106 changes: 91 additions & 15 deletions src/Metadata/Driver/YamlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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'];
Expand All @@ -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);
Expand Down
67 changes: 51 additions & 16 deletions tests/Metadata/Driver/YamlDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -49,19 +51,19 @@ 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';
$p->type = ['name' => 'string', 'params' => []];
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);

Expand All @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions tests/Metadata/Driver/yml/invalid_metadata/BlogPost.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
JMS\Serializer\Tests\Fixtures\Person:
custom_accessor_order: ["age", "name"]

properties:
age: ~
name: ~
7 changes: 7 additions & 0 deletions tests/Metadata/Driver/yml/multiple_types/BlogPost.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
JMS\Serializer\Tests\Fixtures\BlogPost:
xml_root_name: blog-post
exclusion_policy: NONE
properties:
title:
type: string
exclude: true
4 changes: 4 additions & 0 deletions tests/Metadata/Driver/yml/multiple_types/Person.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
JMS\Serializer\Tests\Fixtures\Person:
exclusion_policy: ALL
properties:
name: ~

0 comments on commit 51bbf35

Please sign in to comment.