Skip to content

Allow to use container parameters as class name #2233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ parameters:
# https://github.com/doctrine/doctrine2/pull/7298/files
- '#Strict comparison using === between null and int will always evaluate to false\.#'
- '#Strict comparison using !== between null and null will always evaluate to false\.#'
- '#Class ApiPlatform\\Core\\Tests\\Fixtures\\TestBundle\\Entity\\DummyBis not found.#'

# Expected, due to deprecations
- '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::getResult\(\) invoked with 4 parameters, 1 required\.#'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,15 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
));
}

$container->getDefinition('api_platform.metadata.extractor.xml')->addArgument($xmlResources);
$container->getDefinition('api_platform.metadata.extractor.xml')->replaceArgument(0, $xmlResources);

if (class_exists(Annotation::class)) {
$loader->load('metadata/annotation.xml');
}

if (class_exists(Yaml::class)) {
$loader->load('metadata/yaml.xml');
$container->getDefinition('api_platform.metadata.extractor.yaml')->addArgument($yamlResources);
$container->getDefinition('api_platform.metadata.extractor.yaml')->replaceArgument(0, $yamlResources);
}

if (interface_exists(DocBlockFactoryInterface::class)) {
Expand Down
5 changes: 4 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/metadata/xml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="api_platform.metadata.extractor.xml" class="ApiPlatform\Core\Metadata\Extractor\XmlExtractor" public="false" />
<service id="api_platform.metadata.extractor.xml" class="ApiPlatform\Core\Metadata\Extractor\XmlExtractor" public="false">
<argument type="collection"></argument>
<argument type="service" id="service_container" />
</service>

<service id="api_platform.metadata.resource.name_collection_factory" alias="api_platform.metadata.resource.name_collection_factory.xml" />
<service id="api_platform.metadata.resource.name_collection_factory.xml" class="ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceNameCollectionFactory" public="false">
Expand Down
5 changes: 4 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="api_platform.metadata.extractor.yaml" class="ApiPlatform\Core\Metadata\Extractor\YamlExtractor" public="false" />
<service id="api_platform.metadata.extractor.yaml" class="ApiPlatform\Core\Metadata\Extractor\YamlExtractor" public="false">
<argument type="collection"></argument>
<argument type="service" id="service_container" />
</service>

<service id="api_platform.metadata.resource.name_collection_factory.yaml" decorates="api_platform.metadata.resource.name_collection_factory" class="ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceNameCollectionFactory" public="false">
<argument type="service" id="api_platform.metadata.extractor.yaml" />
Expand Down
74 changes: 73 additions & 1 deletion src/Metadata/Extractor/AbstractExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\Core\Metadata\Extractor;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;

/**
* Base file extractor.
*
Expand All @@ -22,13 +25,16 @@ abstract class AbstractExtractor implements ExtractorInterface
{
protected $paths;
protected $resources;
private $container;
private $collectedParameters = [];

/**
* @param string[] $paths
*/
public function __construct(array $paths)
public function __construct(array $paths, ContainerInterface $container = null)
{
$this->paths = $paths;
$this->container = $container;
}

/**
Expand All @@ -52,4 +58,70 @@ public function getResources(): array
* Extracts metadata from a given path.
*/
abstract protected function extractPath(string $path);

/**
* Recursively replaces placeholders with the service container parameters.
*
* @see https://github.com/symfony/symfony/blob/6fec32c/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php
*
* @copyright (c) Fabien Potencier <[email protected]>
*
* @param mixed $value The source which might contain "%placeholders%"
*
* @throws \RuntimeException When a container value is not a string or a numeric value
*
* @return mixed The source with the placeholders replaced by the container
* parameters. Arrays are resolved recursively.
*/
protected function resolve($value)
{
if (null === $this->container) {
return $value;
}

if (\is_array($value)) {
foreach ($value as $key => $val) {
$value[$key] = $this->resolve($val);
}

return $value;
}

if (!\is_string($value)) {
return $value;
}

$escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value) {
$parameter = $match[1];

// skip %%
if (!isset($parameter)) {
return '%%';
}

if (preg_match('/^env\(\w+\)$/', $parameter)) {
throw new \RuntimeException(sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter));
}

if (array_key_exists($parameter, $this->collectedParameters)) {
return $this->collectedParameters[$parameter];
}

if ($this->container instanceof SymfonyContainerInterface) {
$resolved = $this->container->getParameter($parameter);
} else {
$resolved = $this->container->get($parameter);
}

if (\is_string($resolved) || is_numeric($resolved)) {
$this->collectedParameters[$parameter] = $resolved;

return (string) $resolved;
}

throw new\ RuntimeException(sprintf('The container parameter "%s", used in the resource configuration value "%s", must be a string or numeric, but it is of type %s.', $parameter, $value, \gettype($resolved)));
}, $value);

return str_replace('%%', '%', $escapedValue);
}
}
2 changes: 1 addition & 1 deletion src/Metadata/Extractor/XmlExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected function extractPath(string $path)
}

foreach ($xml->resource as $resource) {
$resourceClass = (string) $resource['class'];
$resourceClass = $this->resolve((string) $resource['class']);

$this->resources[$resourceClass] = [
'shortName' => $this->phpize($resource, 'shortName', 'string'),
Expand Down
4 changes: 3 additions & 1 deletion src/Metadata/Extractor/YamlExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ protected function extractPath(string $path)
private function extractResources(array $resourcesYaml, string $path)
{
foreach ($resourcesYaml as $resourceName => $resourceYaml) {
$resourceName = $this->resolve($resourceName);

if (null === $resourceYaml) {
$resourceYaml = [];
}
Expand Down Expand Up @@ -108,7 +110,7 @@ private function extractProperties(array $resourceYaml, string $resourceName, st
'required' => $this->phpize($propertyValues, 'required', 'bool'),
'identifier' => $this->phpize($propertyValues, 'identifier', 'bool'),
'iri' => $this->phpize($propertyValues, 'iri', 'string'),
'attributes' => $propertyValues['attributes'] ?? null,
'attributes' => $propertyValues['attributes'] ?? [],
'subresource' => $propertyValues['subresource'] ?? null,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ private function getBaseContainerBuilderProphecy()

foreach (['yaml', 'xml'] as $format) {
$definitionProphecy = $this->prophesize(Definition::class);
$definitionProphecy->addArgument(Argument::type('array'))->shouldBeCalled();
$definitionProphecy->replaceArgument(0, Argument::type('array'))->shouldBeCalled();
$containerBuilderProphecy->getDefinition('api_platform.metadata.extractor.'.$format)->willReturn($definitionProphecy->reveal())->shouldBeCalled();
}

Expand Down
10 changes: 10 additions & 0 deletions tests/Fixtures/FileConfigurations/empty-operation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resources:
'App\Entity\Greeting':
collectionOperations:
get:
filters:
- 'greeting.search_filter'
post: ~
itemOperations:
get: ~
put: ~
7 changes: 7 additions & 0 deletions tests/Fixtures/FileConfigurations/resources_empty.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" ?>

<resources xmlns="https://api-platform.com/schema/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://api-platform.com/schema/metadata
https://api-platform.com/schema/metadata/metadata-2.0.xsd">
</resources>
82 changes: 82 additions & 0 deletions tests/Fixtures/FileConfigurations/resources_with_parameters.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" ?>

<resources xmlns="https://api-platform.com/schema/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://api-platform.com/schema/metadata
https://api-platform.com/schema/metadata/metadata-2.0.xsd">
<resource class="%dummy_class%"/>
<resource class="%dummy_class%Bis"/>
<resource
class="%file_config_dummy_class%"
shortName="thedummyshortname"
description="Dummy resource"
iri="someirischema"
>
<itemOperations>
<itemOperation name="my_op_name">
<attribute name="method">GET</attribute>
</itemOperation>
<itemOperation name="my_other_op_name">
<attribute name="method">POST</attribute>
</itemOperation>
</itemOperations>
<collectionOperations>
<collectionOperation name="my_collection_op">
<attribute name="method">POST</attribute>
<attribute name="path">the/collection/path</attribute>
</collectionOperation>
</collectionOperations>
<subresourceOperations>
<subresourceOperation name="my_collection_subresource">
<attribute name="path">the/subresource/path</attribute>
</subresourceOperation>
</subresourceOperations>
<graphql>
<operation name="query">
<attribute name="normalization_context">
<attribute name="groups">
<attribute>graphql</attribute>
</attribute>
</attribute>
</operation>
</graphql>
<attribute name="normalization_context">
<attribute name="groups">
<attribute>default</attribute>
</attribute>
</attribute>
<attribute name="denormalization_context">
<attribute name="groups">
<attribute>default</attribute>
</attribute>
</attribute>
<attribute name="hydra_context">
<attribute name="@type">hydra:Operation</attribute>
<attribute name="@hydra:title">File config Dummy</attribute>
</attribute>

<property
name="foo"
description="The dummy foo"
readable="true"
writable="true"
readableLink="false"
writableLink="false"
required="true"
>
<subresource collection="true" resourceClass="Foo" maxDepth="1" />
<attribute name="foo">
<attribute>Foo</attribute>
</attribute>
<attribute name="bar">
<attribute>
<attribute>Bar</attribute>
</attribute>
<attribute name="baz">Baz</attribute>
</attribute>
<attribute name="baz">Baz</attribute>
</property>

<property name="name" description="The dummy name"/>
</resource>
</resources>
50 changes: 50 additions & 0 deletions tests/Fixtures/FileConfigurations/resources_with_parameters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
resources:
'%dummy_class%': ~
'%dummy_class%Bis': ~
'%file_config_dummy_class%':
shortName: 'thedummyshortname'
description: 'Dummy resource'
itemOperations:
my_op_name:
method: 'GET'
my_other_op_name:
method: 'POST'
collectionOperations:
my_collection_op:
method: 'POST'
path: 'the/collection/path'
subresourceOperations:
my_collection_subresource:
path: 'the/subresource/path'
graphql:
query:
normalization_context:
groups: ['graphql']
attributes:
normalization_context:
groups: ['default']
denormalization_context:
groups: ['default']
hydra_context:
'@type': 'hydra:Operation'
'@hydra:title': 'File config Dummy'
# Use this syntax with Symfony YAML 3.4+:
#'@hydra:title': !php/const ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy::HYDRA_TITLE
iri: 'someirischema'
properties:
'foo':
subresource: {collection: true, resourceClass: 'Foo', maxDepth: 1}
description: 'The dummy foo'
readable: true
writable: true
readableLink: false
writableLink: false
required: true
attributes:
'foo': ['Foo']
'bar':
'0': ['Bar']
'baz': 'Baz'
'baz': 'Baz'
'name':
description: 'The dummy name'
2 changes: 2 additions & 0 deletions tests/Fixtures/FileConfigurations/resourcesinvalid_2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
resources:
'Foo': 'invalid'
3 changes: 3 additions & 0 deletions tests/Fixtures/FileConfigurations/resourcesinvalid_3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resources:
'Foo':
properties: 'invalid'
4 changes: 4 additions & 0 deletions tests/Fixtures/FileConfigurations/resourcesinvalid_4.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resources:
'Foo':
properties:
myprop: 'invalid'
Loading