Skip to content

Commit 8b6e73a

Browse files
External routing files (#1035)
| Q | A | --------------- | ----- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Related tickets | fixes #1029 | License | MIT Very similar to api-platform/core#7017 but in Sylius resource context (for HTML routing) ```php <?php // config/sylius/resources/speaker.php declare(strict_types=1); use Sylius\Resource\Metadata\Create; use Sylius\Resource\Metadata\ResourceMetadata; use Sylius\Resource\Metadata\Update; use Sylius\Resource\Metadata\Operations; use App\Entity\Speaker; return new ResourceMetadata() ->withClass(Speaker::class) ->withOperations(new Operations([ new Create(), new Update(), ])) ; ```
2 parents 2424c56 + 4bd4943 commit 8b6e73a

37 files changed

+1066
-15
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ jobs:
183183
-
184184
name: Create-project with skeleton
185185
run: |
186+
set -x
186187
composer create-project --ansi "symfony/skeleton:${{ matrix.skeleton }}" skeleton_app
187188
cd skeleton_app
188189
composer config extra.symfony.allow-contrib true

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ parameters:
6262
- '/Method Sylius\\Bundle\\ResourceBundle\\Event\\ResourceControllerEvent::stop\(\) has no return typehint specified./'
6363
- '/Method Sylius\\Bundle\\ResourceBundle\\Form\\Extension\\HttpFoundation\\HttpFoundationRequestHandler::handleRequest\(\) has no return typehint specified./'
6464
- '/Method Sylius\\Bundle\\ResourceBundle\\Grid\\Controller\\ResourcesResolver::getResources\(\) has no return typehint specified./'
65+
- '/Method Sylius\\Resource\\Metadata\\Extractor\\AbstractResourceExtractor::getResources\(\) should return array<Sylius\\Resource\\Metadata\\ResourceMetadata> but returns array<Sylius\\Resource\\Metadata\\ResourceMetadata>\|null./'
6566
- '/Method Sylius\\Resource\\Model\\ResourceInterface::getId\(\) has no return typehint specified./'
6667
- '/Method Sylius\\Resource\\Model\\TimestampableInterface::setCreatedAt\(\) has no return typehint specified./'
6768
- '/Method Sylius\\Resource\\Model\\TimestampableInterface::setUpdatedAt\(\) has no return typehint specified./'

psalm.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
<InvalidReturnType>
121121
<errorLevel type="suppress">
122122
<file name="src/Component/src/Doctrine/Persistence/InMemoryRepository.php" />
123+
<file name="src/Component/src/Metadata/Extractor/PhpFileResourceExtractor.php" />
123124
</errorLevel>
124125
</InvalidReturnType>
125126

@@ -173,6 +174,12 @@
173174
</errorLevel>
174175
</MoreSpecificImplementedParamType>
175176

177+
<NullableReturnStatement>
178+
<errorLevel type="suppress">
179+
<file name="src/Component/src/Metadata/Extractor/PhpFileResourceExtractor.php" />
180+
</errorLevel>
181+
</NullableReturnStatement>
182+
176183
<NullArgument>
177184
<errorLevel type="suppress">
178185
<directory name="src" />
@@ -321,6 +328,12 @@
321328
</errorLevel>
322329
</UnrecognizedStatement>
323330

331+
<UnresolvableInclude>
332+
<errorLevel type="suppress">
333+
<file name="src/Component/src/Metadata/Extractor/PhpFileResourceExtractor.php" />
334+
</errorLevel>
335+
</UnresolvableInclude>
336+
324337
<UnsupportedReferenceUsage>
325338
<errorLevel type="suppress">
326339
<file name="src/Component/src/Doctrine/Persistence/InMemoryRepository.php" />

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/DependencyInjection/SyliusResourceExtension.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@
3333
use Sylius\Resource\Twig\Context\Factory\ContextFactoryInterface;
3434
use Symfony\Component\Config\FileLocator;
3535
use Symfony\Component\Config\Loader\LoaderInterface;
36+
use Symfony\Component\Config\Resource\DirectoryResource;
3637
use Symfony\Component\DependencyInjection\ContainerBuilder;
3738
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
39+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
3840
use Symfony\Component\DependencyInjection\Extension\Extension;
3941
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
4042
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
43+
use Symfony\Component\Finder\Finder;
4144
use function Symfony\Component\String\u;
4245

4346
final class SyliusResourceExtension extends Extension implements PrependExtensionInterface
@@ -66,6 +69,7 @@ public function load(array $configs, ContainerBuilder $container): void
6669
$container->setParameter('sylius.resource.settings', $config['settings']);
6770
$container->setAlias('sylius.resource_controller.authorization_checker', $config['authorization_checker']);
6871

72+
$this->registerMetadataConfiguration($container, $config);
6973
$this->autoRegisterResources($config, $container);
7074

7175
$this->loadPersistence($config['drivers'], $config['resources'], $loader, $container);
@@ -320,4 +324,43 @@ private function loadResources(array $loadedResources, ContainerBuilder $contain
320324
}
321325
}
322326
}
327+
328+
private function registerMetadataConfiguration(ContainerBuilder $container, array $config): void
329+
{
330+
$resources = $this->getResourceFilesToWatch($container, $config);
331+
332+
$container->getDefinition('sylius.metadata.resource_extractor.php_file')->replaceArgument(0, $resources);
333+
}
334+
335+
private function getResourceFilesToWatch(ContainerBuilder $container, array $config): array
336+
{
337+
$files = [];
338+
339+
/** @var string $path */
340+
foreach ($config['mapping']['imports'] ?? [] as $path) {
341+
if (is_dir($path)) {
342+
foreach (Finder::create()->followLinks()->files()->in($path)->name('/\.php$/')->sortByName() as $file) {
343+
$files[] = $file->getRealPath();
344+
}
345+
346+
$container->addResource(new DirectoryResource($path, '/\.php$/'));
347+
348+
continue;
349+
}
350+
351+
if ($container->fileExists($path, false)) {
352+
if (!str_ends_with($path, '.php')) {
353+
throw new RuntimeException(\sprintf('Unsupported mapping type in "%s", supported type is PHP.', $path));
354+
}
355+
356+
$files[] = $path;
357+
358+
continue;
359+
}
360+
361+
throw new RuntimeException(\sprintf('Could not open file or directory "%s".', $path));
362+
}
363+
364+
return $files;
365+
}
323366
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
2+
<services>
3+
<service id="sylius.metadata.resource_extractor.php_file" class="Sylius\Resource\Metadata\Extractor\PhpFileResourceExtractor" public="false">
4+
<argument type="collection" />
5+
<argument type="service" id="service_container" />
6+
</service>
7+
</services>
8+
</container>

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.metadata.resource_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: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,28 @@
1717
<tag name="cache.pool" />
1818
</service>
1919

20+
<service id="sylius.resource_metadata_collection.factory" alias="sylius.resource_metadata_collection.factory.attributes" />
21+
<service id="Sylius\Resource\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface" alias="sylius.resource_metadata_collection.factory.attributes" />
22+
2023
<service id="sylius.resource_metadata_collection.factory.attributes" class="Sylius\Resource\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory">
2124
<argument type="service" id="sylius.resource_registry" />
2225
<argument type="service" id="sylius.routing.factory.operation_route_name_factory" />
2326
</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" />
27+
28+
<service id="sylius.resource_metadata_collection.factory.php_file"
29+
class="Sylius\Resource\Metadata\Resource\Factory\PhpFileResourceMetadataCollectionFactory"
30+
decorates="sylius.resource_metadata_collection.factory"
31+
decoration-priority="400"
32+
>
33+
<argument type="service" id="sylius.resource_registry" />
34+
<argument type="service" id="sylius.routing.factory.operation_route_name_factory" />
35+
<argument type="service" id="sylius.metadata.resource_extractor.php_file" />
36+
<argument type="service" id=".inner" />
37+
</service>
2638

2739
<service id="sylius.resource_metadata_collection.factory.state_machine"
2840
class="Sylius\Resource\Metadata\Resource\Factory\StateMachineResourceMetadataCollectionFactory"
29-
decorates="sylius.resource_metadata_collection.factory.attributes"
41+
decorates="sylius.resource_metadata_collection.factory"
3042
decoration-priority="300"
3143
>
3244
<argument type="service" id="sylius.resource_registry" />
@@ -83,15 +95,15 @@
8395

8496
<service id="sylius.resource_metadata_collection.factory.templates_dir"
8597
class="Sylius\Resource\Metadata\Resource\Factory\TemplatesDirResourceMetadataCollectionFactory"
86-
decorates="sylius.resource_metadata_collection.factory.attributes"
98+
decorates="sylius.resource_metadata_collection.factory"
8799
>
88100
<argument type="service" id=".inner" />
89101
<argument>%sylius.resource.settings%</argument>
90102
</service>
91103

92104
<service id="sylius.resource_metadata_collection.factory.cached"
93105
class="Sylius\Resource\Metadata\Resource\Factory\CachedResourceMetadataCollectionFactory"
94-
decorates="sylius.resource_metadata_collection.factory.attributes"
106+
decorates="sylius.resource_metadata_collection.factory"
95107
decoration-priority="-10"
96108
>
97109
<argument type="service" id="sylius.cache.metadata.resource_collection" />
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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\Exception\RuntimeException;
18+
use Sylius\Resource\Metadata\ResourceMetadata;
19+
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
20+
21+
/**
22+
* Base file extractor.
23+
*
24+
* @experimental
25+
*/
26+
abstract class AbstractResourceExtractor implements ResourceExtractorInterface
27+
{
28+
/** @var ResourceMetadata[]|null */
29+
protected ?array $resources = null;
30+
31+
private array $collectedParameters = [];
32+
33+
/** @param string[] $paths */
34+
public function __construct(
35+
protected array $paths,
36+
private readonly ?ContainerInterface $container = null,
37+
) {
38+
}
39+
40+
/**
41+
* @inheritdoc
42+
*/
43+
public function getResources(): array
44+
{
45+
if (null !== $this->resources) {
46+
return $this->resources;
47+
}
48+
49+
$this->resources = [];
50+
foreach ($this->paths as $path) {
51+
$this->extractFromPath($path);
52+
}
53+
54+
return $this->resources;
55+
}
56+
57+
/**
58+
* Extracts metadata from a given path.
59+
*/
60+
abstract protected function extractFromPath(string $path): void;
61+
62+
/**
63+
* Recursively replaces placeholders with the service container parameters.
64+
*
65+
* @see https://github.com/symfony/symfony/blob/6fec32c/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php
66+
*
67+
* @param mixed $value The source which might contain "%placeholders%"
68+
*
69+
* @throws \RuntimeException When a container value is not a string or a numeric value
70+
*
71+
* @return mixed The source with the placeholders replaced by the container
72+
* parameters. Arrays are resolved recursively.
73+
*/
74+
protected function resolve(mixed $value): mixed
75+
{
76+
if (null === $this->container) {
77+
return $value;
78+
}
79+
80+
if (\is_array($value)) {
81+
foreach ($value as $key => $val) {
82+
$value[$key] = $this->resolve($val);
83+
}
84+
85+
return $value;
86+
}
87+
88+
if (!\is_string($value)) {
89+
return $value;
90+
}
91+
92+
$escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value) {
93+
$parameter = $match[1] ?? null;
94+
95+
// skip %%
96+
if (!isset($parameter)) {
97+
return '%%';
98+
}
99+
100+
if (preg_match('/^env\(\w+\)$/', $parameter)) {
101+
throw new RuntimeException(\sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter));
102+
}
103+
104+
if (\array_key_exists($parameter, $this->collectedParameters)) {
105+
return $this->collectedParameters[$parameter];
106+
}
107+
108+
if ($this->container instanceof SymfonyContainerInterface) {
109+
$resolved = $this->container->getParameter($parameter);
110+
} else {
111+
$resolved = $this->container?->get($parameter);
112+
}
113+
114+
if (\is_string($resolved) || is_numeric($resolved)) {
115+
$this->collectedParameters[$parameter] = $resolved;
116+
117+
return (string) $resolved;
118+
}
119+
120+
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)));
121+
}, $value);
122+
123+
return str_replace('%%', '%', $escapedValue ?? '');
124+
}
125+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
/**
19+
* @experimental
20+
*/
21+
final class PhpFileResourceExtractor extends AbstractResourceExtractor
22+
{
23+
protected function extractFromPath(string $path): void
24+
{
25+
$resource = $this->getPHPFileClosure($path)();
26+
27+
if (!$resource instanceof ResourceMetadata) {
28+
return;
29+
}
30+
31+
$resourceReflection = new \ReflectionClass($resource);
32+
33+
foreach ($resourceReflection->getProperties() as $property) {
34+
$property->setAccessible(true);
35+
$resolvedValue = $this->resolve($property->getValue($resource));
36+
$property->setValue($resource, $resolvedValue);
37+
}
38+
39+
$this->resources[] = $resource;
40+
}
41+
42+
/**
43+
* Scope isolated include.
44+
*
45+
* Prevents access to $this/self from included files.
46+
*/
47+
private function getPHPFileClosure(string $filePath): \Closure
48+
{
49+
return \Closure::bind(function () use ($filePath): mixed {
50+
return require $filePath;
51+
}, null, null);
52+
}
53+
}

0 commit comments

Comments
 (0)