Skip to content

Commit f95a255

Browse files
authored
[Metadata] Add PHP file resource extractor (#1004)
| Q | A | --------------- | ----- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Related tickets | | License | MIT Same feature as I did on api-platform/core#7017
2 parents 85b879b + 910820f commit f95a255

File tree

10 files changed

+309
-1
lines changed

10 files changed

+309
-1
lines changed

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" />
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+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
* Extracts an array of metadata from a file or a list of files.
20+
*
21+
* @experimental
22+
*/
23+
interface ResourceExtractorInterface
24+
{
25+
/**
26+
* Parses all metadata files and convert them in an array.
27+
*
28+
* @return ResourceMetadata[]
29+
*/
30+
public function getResources(): array;
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Tests\Metadata\Extractor;
15+
16+
use PHPUnit\Framework\TestCase;
17+
use Sylius\Resource\Metadata\Extractor\PhpFileResourceExtractor;
18+
use Sylius\Resource\Metadata\ResourceMetadata;
19+
20+
final class PhpFileResourceExtractorTest extends TestCase
21+
{
22+
public function testItGetsResourcesFromPhpFileThatReturnsResourceMetadata(): void
23+
{
24+
$extractor = new PhpFileResourceExtractor([__DIR__ . '/php/valid_php_file.php', __DIR__ . '/php/another_valid_php_file.php']);
25+
26+
$expectedResources = [new ResourceMetadata(alias: 'dummy'), new ResourceMetadata(alias: 'another_dummy')];
27+
28+
$this->assertEquals($expectedResources, $extractor->getResources());
29+
}
30+
31+
public function testItExcludesResourcesFromPhpFileThatDoesNotReturnResourceMetadata(): void
32+
{
33+
$extractor = new PhpFileResourceExtractor([__DIR__ . '/php/invalid_php_file.php']);
34+
35+
$this->assertEquals([], $extractor->getResources());
36+
}
37+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
use Sylius\Resource\Metadata\ResourceMetadata;
15+
16+
return new ResourceMetadata(alias: 'another_dummy');
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
use Sylius\Resource\Tests\Dummy\PullRequest;
15+
16+
return new PullRequest();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
use Sylius\Resource\Metadata\ResourceMetadata;
15+
16+
return new ResourceMetadata(alias: 'dummy');

tests/Application/config/packages/framework.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ framework:
66
secret: "%secret%"
77
form: ~
88
csrf_protection: true
9-
default_locale: "%locale%"
9+
enabled_locales: ["%locale%"]
1010
session:
1111
handler_id: ~
1212
storage_factory_id: session.storage.factory.mock_file

0 commit comments

Comments
 (0)