Skip to content

Commit 10224da

Browse files
committed
feat(metadata) Load external PHP file resources
1 parent bf09616 commit 10224da

11 files changed

+278
-3
lines changed

Diff for: src/Metadata/Extractor/PhpFileResourceExtractor.php

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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 ApiPlatform\Metadata\Extractor;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
18+
/**
19+
* Extracts an array of metadata from a list of PHP files.
20+
*
21+
* @author Loïc Frémont <[email protected]>
22+
*/
23+
final class PhpFileResourceExtractor extends AbstractResourceExtractor
24+
{
25+
use ResourceExtractorTrait;
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
protected function extractPath(string $path): void
31+
{
32+
$resource = $this->getPHPFileClosure($path)();
33+
34+
if (!$resource instanceof ApiResource) {
35+
return;
36+
}
37+
38+
$resourceReflection = new \ReflectionClass($resource);
39+
40+
foreach ($resourceReflection->getProperties() as $property) {
41+
$property->setAccessible(true);
42+
$resolvedValue = $this->resolve($property->getValue($resource));
43+
$property->setValue($resource, $resolvedValue);
44+
}
45+
46+
$this->resources = [$resource];
47+
}
48+
49+
/**
50+
* Scope isolated include.
51+
*
52+
* Prevents access to $this/self from included files.
53+
*/
54+
private function getPHPFileClosure(string $filePath): \Closure
55+
{
56+
return \Closure::bind(function () use ($filePath): mixed {
57+
return require $filePath;
58+
}, null, null);
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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 ApiPlatform\Metadata\Resource\Factory;
15+
16+
use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Operations;
19+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
20+
21+
final class PhpFileResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
22+
{
23+
use OperationDefaultsTrait;
24+
25+
public function __construct(
26+
private readonly ResourceExtractorInterface $metadataExtractor,
27+
private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null,
28+
) {
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function create(string $resourceClass): ResourceMetadataCollection
35+
{
36+
$resourceMetadataCollection = new ResourceMetadataCollection($resourceClass);
37+
if ($this->decorated) {
38+
$resourceMetadataCollection = $this->decorated->create($resourceClass);
39+
}
40+
41+
foreach ($this->metadataExtractor->getResources() as $resource) {
42+
if ($resourceClass !== $resource->getClass()) {
43+
continue;
44+
}
45+
46+
$shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
47+
$resource = $this->getResourceWithDefaults($resourceClass, $shortName, $resource);
48+
49+
$operations = [];
50+
/** @var Operation $operation */
51+
foreach ($resource->getOperations() ?? new Operations() as $operation) {
52+
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
53+
$operations[$key] = $operation;
54+
}
55+
56+
if ($operations) {
57+
$resource = $resource->withOperations(new Operations($operations));
58+
}
59+
60+
$resourceMetadataCollection[] = $resource;
61+
}
62+
63+
return $resourceMetadataCollection;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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 ApiPlatform\Metadata\Resource\Factory;
15+
16+
use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface;
17+
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
18+
19+
/**
20+
* @internal
21+
*/
22+
final class PhpFileResourceNameCollectionFactory implements ResourceNameCollectionFactoryInterface
23+
{
24+
public function __construct(
25+
private readonly ResourceExtractorInterface $metadataExtractor,
26+
private readonly ?ResourceNameCollectionFactoryInterface $decorated = null,
27+
) {
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function create(): ResourceNameCollection
34+
{
35+
$classes = [];
36+
37+
if ($this->decorated) {
38+
foreach ($this->decorated->create() as $resourceClass) {
39+
$classes[$resourceClass] = true;
40+
}
41+
}
42+
43+
foreach ($this->metadataExtractor->getResources() as $resource) {
44+
$resourceClass = $resource->getClass();
45+
46+
if (null === $resourceClass) {
47+
continue;
48+
}
49+
50+
$classes[$resourceClass] = true;
51+
}
52+
53+
return new ResourceNameCollection(array_keys($classes));
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiPlatform\Metadata\Tests\Extractor;
6+
7+
use ApiPlatform\Metadata\ApiResource;
8+
use ApiPlatform\Metadata\Extractor\PhpFileResourceExtractor;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class PhpFileResourceExtractorTest extends TestCase
12+
{
13+
public function testItGetsResourcesFromPhpFileThatReturnsAnApiResource(): void
14+
{
15+
$extractor = new PhpFileResourceExtractor([__DIR__ . '/php/valid_php_file.php']);
16+
17+
$expectedResource = new ApiResource(shortName: 'dummy');
18+
19+
$this->assertEquals([$expectedResource], $extractor->getResources());
20+
}
21+
22+
public function testItExcludesResourcesFromPhpFileThatDoesNotReturnAnApiResource(): void
23+
{
24+
$extractor = new PhpFileResourceExtractor([__DIR__ . '/php/invalid_php_file.php']);
25+
26+
$this->assertEquals([], $extractor->getResources());
27+
}
28+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
return new \stdClass();

Diff for: src/Metadata/Tests/Extractor/php/valid_php_file.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use ApiPlatform\Metadata\ApiResource;
6+
7+
return new ApiResource(shortName: 'dummy');

Diff for: src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+31-3
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ private function normalizeDefaults(array $defaults): array
305305

306306
private function registerMetadataConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
307307
{
308-
[$xmlResources, $yamlResources] = $this->getResourcesToWatch($container, $config);
308+
[$xmlResources, $yamlResources, $phpResources] = $this->getResourcesToWatch($container, $config);
309309

310310
$container->setParameter('api_platform.class_name_resources', $this->getClassNameResources());
311311

@@ -320,6 +320,7 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
320320
}
321321

322322
// V3 metadata
323+
$loader->load('metadata/php.xml');
323324
$loader->load('metadata/xml.xml');
324325
$loader->load('metadata/links.xml');
325326
$loader->load('metadata/property.xml');
@@ -338,6 +339,8 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
338339
$container->getDefinition('api_platform.metadata.resource_extractor.yaml')->replaceArgument(0, $yamlResources);
339340
$container->getDefinition('api_platform.metadata.property_extractor.yaml')->replaceArgument(0, $yamlResources);
340341
}
342+
343+
$container->getDefinition('api_platform.metadata.resource_extractor.php_file')->replaceArgument(0, $phpResources);
341344
}
342345

343346
private function getClassNameResources(): array
@@ -402,7 +405,32 @@ private function getResourcesToWatch(ContainerBuilder $container, array $config)
402405
}
403406
}
404407

405-
$resources = ['yml' => [], 'xml' => [], 'dir' => []];
408+
$resources = ['yml' => [], 'xml' => [], 'php' => [], 'dir' => []];
409+
410+
foreach ($config['mapping']['imports'] ?? [] as $path) {
411+
if (is_dir($path)) {
412+
foreach (Finder::create()->followLinks()->files()->in($path)->name('/\.php$/')->sortByName() as $file) {
413+
$resources[$file->getExtension()][] = $file->getRealPath();
414+
}
415+
416+
$resources['dir'][] = $path;
417+
$container->addResource(new DirectoryResource($path, '/\.php$/'));
418+
419+
continue;
420+
}
421+
422+
if ($container->fileExists($path, false)) {
423+
if (!preg_match('/\.php$/', (string) $path, $matches)) {
424+
throw new RuntimeException(\sprintf('Unsupported mapping type in "%s", supported type is PHP.', $path));
425+
}
426+
427+
$resources['php' === $matches[1]][] = $path;
428+
429+
continue;
430+
}
431+
432+
throw new RuntimeException(\sprintf('Could not open file or directory "%s".', $path));
433+
}
406434

407435
foreach ($paths as $path) {
408436
if (is_dir($path)) {
@@ -431,7 +459,7 @@ private function getResourcesToWatch(ContainerBuilder $container, array $config)
431459

432460
$container->setParameter('api_platform.resource_class_directories', $resources['dir']);
433461

434-
return [$resources['xml'], $resources['yml']];
462+
return [$resources['xml'], $resources['yml'], $resources['php']];
435463
}
436464

437465
private function registerOAuthConfiguration(ContainerBuilder $container, array $config): void

Diff for: src/Symfony/Bundle/DependencyInjection/Configuration.php

+3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ public function getConfigTreeBuilder(): TreeBuilder
134134
->arrayNode('mapping')
135135
->addDefaultsIfNotSet()
136136
->children()
137+
->arrayNode('imports')
138+
->prototype('scalar')->end()
139+
->end()
137140
->arrayNode('paths')
138141
->prototype('scalar')->end()
139142
->end()

Diff for: src/Symfony/Bundle/Resources/config/metadata/php.xml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="api_platform.metadata.resource_extractor.php_file" class="ApiPlatform\Metadata\Extractor\PhpFileResourceExtractor" public="false">
9+
<argument type="collection" />
10+
<argument type="service" id="service_container" />
11+
</service>
12+
</services>
13+
</container>

Diff for: src/Symfony/Bundle/Resources/config/metadata/resource.xml

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
<argument>%api_platform.graphql.enabled%</argument>
2525
</service>
2626

27+
<service id="api_platform.metadata.resource.metadata_collection_factory.php_file" class="ApiPlatform\Metadata\Resource\Factory\PhpFileResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="800" public="false">
28+
<argument type="service" id="api_platform.metadata.resource_extractor.php_file" />
29+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.php_file.inner" />
30+
<argument type="service" id="service_container" on-invalid="null" />
31+
</service>
32+
2733
<service id="api_platform.metadata.resource.metadata_collection_factory.concerns" class="ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="800" public="false">
2834
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.concerns.inner" />
2935
</service>

Diff for: src/Symfony/Bundle/Resources/config/metadata/resource_name.xml

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
<argument type="service" id="api_platform.metadata.resource_extractor.xml" />
2323
</service>
2424

25+
<service id="api_platform.metadata.resource.name_collection_factory.php_file" class="ApiPlatform\Metadata\Resource\Factory\PhpFileResourceNameCollectionFactory" decorates="api_platform.metadata.resource.name_collection_factory" decoration-priority="900" public="false">
26+
<argument type="service" id="api_platform.metadata.resource_extractor.php_file" />
27+
<argument type="service" id="api_platform.metadata.resource.name_collection_factory.php_file.inner" />
28+
</service>
29+
2530
<service id="api_platform.metadata.resource.name_collection_factory.concerns" class="ApiPlatform\Metadata\Resource\Factory\ConcernsResourceNameCollectionFactory" decorates="api_platform.metadata.resource.name_collection_factory" decoration-priority="800" public="false">
2631
<argument>%api_platform.resource_class_directories%</argument>
2732
<argument type="service" id="api_platform.metadata.resource.name_collection_factory.concerns.inner" />

0 commit comments

Comments
 (0)