Skip to content

Commit 62e635f

Browse files
committed
Add external PHP file configuration for routing
1 parent dff15ee commit 62e635f

File tree

19 files changed

+514
-9
lines changed

19 files changed

+514
-9
lines changed

psalm.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
<file name="src/Bundle/Grid/Parser/OptionsParser.php" />
117117
</errorLevel>
118118
</InvalidReturnStatement>
119+
120+
<InvalidNullableReturnType>
121+
<errorLevel type="suppress">
122+
<file name="src/Component/src/Metadata/Extractor/PhpFileMetadataExtractor.php" />
123+
</errorLevel>
124+
</InvalidNullableReturnType>
119125

120126
<InvalidReturnType>
121127
<errorLevel type="suppress">
@@ -179,6 +185,12 @@
179185
</errorLevel>
180186
</NullArgument>
181187

188+
<NullableReturnStatement>
189+
<errorLevel type="suppress">
190+
<file name="src/Component/src/Metadata/Extractor/PhpFileMetadataExtractor.php" />
191+
</errorLevel>
192+
</NullableReturnStatement>
193+
182194
<PossiblyFalseOperand>
183195
<errorLevel type="suppress">
184196
<file name="src/Component/src/Reflection/ClassInfoTrait.php" />
@@ -320,6 +332,12 @@
320332
<directory name="src/Component/legacy/src/Translation" />
321333
</errorLevel>
322334
</UnrecognizedStatement>
335+
336+
<UnresolvableInclude>
337+
<errorLevel type="suppress">
338+
<file name="src/Component/src/Metadata/Extractor/PhpFileMetadataExtractor.php" />
339+
</errorLevel>
340+
</UnresolvableInclude>
323341

324342
<UnsupportedReferenceUsage>
325343
<errorLevel type="suppress">

src/Bundle/DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public function getConfigTreeBuilder(): TreeBuilder
3939
->arrayNode('mapping')
4040
->addDefaultsIfNotSet()
4141
->children()
42+
->arrayNode('imports')
43+
->prototype('scalar')->end()
44+
->end()
4245
->arrayNode('paths')
4346
->prototype('scalar')->end()
4447
->end()

src/Bundle/Resources/config/services/metadata/resource_class_list.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,15 @@
2222
<argument>%sylius.resource.mapping%</argument>
2323
</service>
2424
<service id="Sylius\Resource\Metadata\Resource\Factory\AttributesResourceClassListFactory" alias="sylius.metadata.resource_class_list.factory.attributes" />
25+
26+
<service id="sylius.metadata.resource_class_list.factory.php_file"
27+
class="Sylius\Resource\Metadata\Resource\Factory\PhpFileResourceClassListFactory"
28+
decorates="sylius.metadata.resource_class_list.factory"
29+
decoration-priority="100"
30+
>
31+
<argument type="service" id="sylius.resource.metadata.extractor.php_file" />
32+
<argument type="service" id=".inner" />
33+
</service>
34+
<service id="Sylius\Resource\Metadata\Resource\Factory\PhpFileResourceClassListFactory" alias="sylius.metadata.resource_class_list.factory.php_file" />
2535
</services>
2636
</container>

src/Bundle/Resources/config/services/metadata/resource_metadata_collection.xml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,29 @@
1717
<tag name="cache.pool" />
1818
</service>
1919

20-
<service id="sylius.resource_metadata_collection.factory.attributes" class="Sylius\Resource\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory">
20+
<service id="sylius.resource.metadata.extractor.php_file" class="Sylius\Resource\Metadata\Extractor\PhpFileMetadataExtractor">
21+
<argument>%sylius.resource.mapping%</argument>
22+
<argument type="service" id="service_container" />
23+
</service>
24+
25+
<service id="sylius.resource_metadata_collection.factory" alias="sylius.resource_metadata_collection.factory.php_file" />
26+
<service id="Sylius\Resource\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface" alias="sylius.resource_metadata_collection.factory.php_file" />
27+
28+
<service id="sylius.resource_metadata_collection.factory.php_file" class="Sylius\Resource\Metadata\Resource\Factory\PhpFileResourceMetadataCollectionFactory">
29+
<argument type="service" id="sylius.resource_registry" />
30+
<argument type="service" id="sylius.routing.factory.operation_route_name_factory" />
31+
<argument type="service" id="sylius.resource.metadata.extractor.php_file" />
32+
</service>
33+
34+
<service id="sylius.resource_metadata_collection.factory.attributes"
35+
class="Sylius\Resource\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory"
36+
decorates="sylius.resource_metadata_collection.factory"
37+
decoration-priority="400"
38+
>
2139
<argument type="service" id="sylius.resource_registry" />
2240
<argument type="service" id="sylius.routing.factory.operation_route_name_factory" />
41+
<argument type="service" id=".inner" />
2342
</service>
24-
<service id="Sylius\Resource\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface" alias="sylius.resource_metadata_collection.factory.attributes" />
25-
<service id="sylius.resource_metadata_collection.factory" alias="sylius.resource_metadata_collection.factory.attributes" />
2643

2744
<service id="sylius.resource_metadata_collection.factory.state_machine"
2845
class="Sylius\Resource\Metadata\Resource\Factory\StateMachineResourceMetadataCollectionFactory"

src/Component/spec/Symfony/Routing/Factory/AttributesOperationRouteFactorySpec.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ function let(
4242
new AttributesResourceMetadataCollectionFactory(
4343
$resourceRegistry->getWrappedObject(),
4444
new OperationRouteNameFactory(),
45-
'symfony',
4645
),
4746
);
4847
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Resource\Metadata\Extractor;
15+
16+
use Sylius\Resource\Metadata\ResourceMetadata;
17+
18+
interface MetadataExtractorInterface
19+
{
20+
/**
21+
* @return ResourceMetadata[]
22+
*/
23+
public function extract(): array;
24+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Resource\Metadata\Extractor;
15+
16+
use Psr\Container\ContainerInterface;
17+
use Sylius\Resource\Metadata\ResourceMetadata;
18+
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
19+
use Symfony\Component\Finder\Finder;
20+
21+
final class PhpFileMetadataExtractor implements MetadataExtractorInterface
22+
{
23+
private array $collectedParameters = [];
24+
25+
public function __construct(
26+
private readonly array $resourceMapping,
27+
private readonly ?ContainerInterface $container = null,
28+
) {
29+
}
30+
31+
/**
32+
* @inheritDoc
33+
*/
34+
public function extract(): array
35+
{
36+
$metadata = [];
37+
38+
foreach ($this->getResourceFilePaths() as $filePath) {
39+
if (!is_readable($filePath)) {
40+
continue;
41+
}
42+
43+
$resource = $this->getPHPFileClosure($filePath)();
44+
45+
if (!$resource instanceof ResourceMetadata) {
46+
continue;
47+
}
48+
49+
$resourceReflection = new \ReflectionClass($resource);
50+
51+
foreach ($resourceReflection->getProperties() as $property) {
52+
$property->setAccessible(true);
53+
$resolvedValue = $this->resolve($property->getValue($resource));
54+
$property->setValue($resource, $resolvedValue);
55+
}
56+
57+
$metadata[] = $resource;
58+
}
59+
60+
return $metadata;
61+
}
62+
63+
private function getResourceFilePaths(): iterable
64+
{
65+
foreach ($this->createFinder() as $file) {
66+
yield $file->getPathname();
67+
}
68+
}
69+
70+
private function createFinder(): Finder
71+
{
72+
$finder = (new Finder())->files();
73+
74+
foreach ($this->resourceMapping['imports'] ?? [] as $path) {
75+
$finder->in($path);
76+
}
77+
78+
return $finder->files();
79+
}
80+
81+
/**
82+
* Scope isolated include.
83+
*
84+
* Prevents access to $this/self from included files.
85+
*/
86+
private function getPHPFileClosure(string $filePath): \Closure
87+
{
88+
return \Closure::bind(function () use ($filePath): mixed {
89+
return require $filePath;
90+
}, null, null);
91+
}
92+
93+
/**
94+
* Recursively replaces placeholders with the service container parameters.
95+
*
96+
* @see https://github.com/symfony/symfony/blob/6fec32c/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php
97+
*
98+
* @param mixed $value The source which might contain "%placeholders%"
99+
*
100+
* @throws \RuntimeException When a container value is not a string or a numeric value
101+
*
102+
* @return mixed The source with the placeholders replaced by the container
103+
* parameters. Arrays are resolved recursively.
104+
*/
105+
private function resolve(mixed $value): mixed
106+
{
107+
$container = $this->container;
108+
109+
if (null === $container) {
110+
return $value;
111+
}
112+
113+
if (\is_array($value)) {
114+
foreach ($value as $key => $val) {
115+
$value[$key] = $this->resolve($val);
116+
}
117+
118+
return $value;
119+
}
120+
121+
if (!\is_string($value)) {
122+
return $value;
123+
}
124+
125+
$escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value, $container) {
126+
$parameter = $match[1] ?? null;
127+
128+
// skip %%
129+
if (!isset($parameter)) {
130+
return '%%';
131+
}
132+
133+
if (preg_match('/^env\(\w+\)$/', $parameter)) {
134+
throw new \RuntimeException(\sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter));
135+
}
136+
137+
if (\array_key_exists($parameter, $this->collectedParameters)) {
138+
return $this->collectedParameters[$parameter];
139+
}
140+
141+
if ($container instanceof SymfonyContainerInterface) {
142+
$resolved = $container->getParameter($parameter);
143+
} else {
144+
$resolved = $container->get($parameter);
145+
}
146+
147+
if (\is_string($resolved) || is_numeric($resolved)) {
148+
$this->collectedParameters[$parameter] = $resolved;
149+
150+
return (string) $resolved;
151+
}
152+
153+
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)));
154+
}, $value);
155+
156+
return str_replace('%%', '%', $escapedValue ?? '');
157+
}
158+
}

src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ final class AttributesResourceMetadataCollectionFactory implements ResourceMetad
2929
public function __construct(
3030
private RegistryInterface $resourceRegistry,
3131
private OperationRouteNameFactory $operationRouteNameFactory,
32+
private ?ResourceMetadataCollectionFactoryInterface $decorated = null,
3233
) {
3334
}
3435

3536
public function create(string $resourceClass): ResourceMetadataCollection
3637
{
3738
$resourceMetadataCollection = new ResourceMetadataCollection();
39+
if ($this->decorated) {
40+
$resourceMetadataCollection = $this->decorated->create($resourceClass);
41+
}
3842

3943
$attributes = ClassReflection::getClassAttributes($resourceClass);
4044

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Resource\Metadata\Resource\Factory;
15+
16+
use Sylius\Resource\Metadata\Extractor\MetadataExtractorInterface;
17+
use Sylius\Resource\Metadata\Resource\ResourceClassList;
18+
19+
final class PhpFileResourceClassListFactory implements ResourceClassListFactoryInterface
20+
{
21+
use OperationDefaultsTrait;
22+
23+
public function __construct(
24+
private readonly MetadataExtractorInterface $metadataExtractor,
25+
private readonly ?ResourceClassListFactoryInterface $decorated = null,
26+
) {
27+
}
28+
29+
public function create(): ResourceClassList
30+
{
31+
$classes = [];
32+
33+
if ($this->decorated) {
34+
foreach ($this->decorated->create() as $resourceClass) {
35+
$classes[$resourceClass] = true;
36+
}
37+
}
38+
39+
foreach ($this->metadataExtractor->extract() as $resource) {
40+
$resourceClass = $resource->getClass();
41+
42+
if (null === $resourceClass) {
43+
continue;
44+
}
45+
46+
$classes[$resourceClass] = true;
47+
}
48+
49+
return new ResourceClassList(array_keys($classes));
50+
}
51+
}

0 commit comments

Comments
 (0)