Skip to content

Commit 119b05b

Browse files
Add XML and YAML support for new Resource attribute (#4382)
* feat: add XML and YAML support for new Resource attribute * feat: add unit tests * feat: add Metadata compatibility tests * feat: support properties out of resources * fix: service inheritance * fix: schema * fix: a function cannot provide a Generator and an object|null * chore: do not load yaml/xml ORM resources on ODM * fix: CI * fix: rollback securityPostValidation on Operation (should be handler in another PR) * fix: mongodb tests * fix: fix ApiLoader legacy operationName * fix: schema validator * fix: graphql BC layer * fix: hal behat scenarios * fix: ignore tests/Core/Behat contexts from PHPStan analysis * fix tests * fix: BC layer Behat tests * fix: yaml services decoration * fix: revert change that broke everything * fix: phpunit tests * fix: read legacy identifiers from properties * feat: move Extractor functions to trait * fix: Behat coverage * fix: coveralls Co-authored-by: soyuka <[email protected]>
1 parent ccfebd4 commit 119b05b

File tree

84 files changed

+5221
-187
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+5221
-187
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ jobs:
138138
run: |
139139
composer remove --dev --no-interaction --no-progress --no-update --ansi \
140140
doctrine/mongodb-odm \
141-
doctrine/mongodb-odm-bundle \
141+
doctrine/mongodb-odm-bundle
142142
- name: Remove ElasticSearch
143143
if: startsWith(matrix.php, '7.1') || startsWith(matrix.php, '7.2')
144144
run: |
145145
composer remove --dev --no-interaction --no-progress --no-update --ansi \
146-
elasticsearch/elasticsearch \
146+
elasticsearch/elasticsearch
147147
- name: Update project dependencies
148148
run: composer update --no-interaction --no-progress --ansi
149149
- name: Require Symfony components
@@ -181,9 +181,9 @@ jobs:
181181
env:
182182
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
183183
run: |
184-
composer global require --prefer-dist --no-interaction --no-progress --ansi cedx/coveralls
184+
composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
185185
export PATH="$PATH:$HOME/.composer/vendor/bin"
186-
coveralls build/logs/phpunit/clover.xml
186+
php-coveralls --coverage_clover=build/logs/phpunit/clover.xml
187187
continue-on-error: true
188188

189189
behat:
@@ -231,12 +231,12 @@ jobs:
231231
run: |
232232
composer remove --dev --no-interaction --no-progress --no-update --ansi \
233233
doctrine/mongodb-odm \
234-
doctrine/mongodb-odm-bundle \
234+
doctrine/mongodb-odm-bundle
235235
- name: Remove ElasticSearch
236236
if: startsWith(matrix.php, '7.1') || startsWith(matrix.php, '7.2')
237237
run: |
238238
composer remove --dev --no-interaction --no-progress --no-update --ansi \
239-
elasticsearch/elasticsearch \
239+
elasticsearch/elasticsearch
240240
- name: Update project dependencies
241241
run: composer update --no-interaction --no-progress --ansi
242242
- name: Require Symfony components
@@ -250,7 +250,7 @@ jobs:
250250
- name: Clear test app cache (php 8.0)
251251
if: (startsWith(matrix.php, '8.'))
252252
run: rm -Rf tests/Fixtures/app/var/cache/*
253-
- name: Run Behat tests
253+
- name: Run Behat tests (PHP < 8)
254254
if: (!startsWith(matrix.php, '8.'))
255255
run: |
256256
mkdir -p build/logs/behat
@@ -263,7 +263,7 @@ jobs:
263263
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@php8'
264264
fi
265265
fi
266-
- name: Run Behat tests
266+
- name: Run Behat tests (PHP 8)
267267
if: (startsWith(matrix.php, '8.'))
268268
run: |
269269
mkdir -p build/logs/behat
@@ -300,9 +300,9 @@ jobs:
300300
env:
301301
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
302302
run: |
303-
composer global require --prefer-dist --no-interaction --no-progress --ansi cedx/coveralls
303+
composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
304304
export PATH="$PATH:$HOME/.composer/vendor/bin"
305-
coveralls build/logs/behat/clover.xml
305+
php-coveralls --coverage_clover=build/logs/behat/clover.xml
306306
continue-on-error: true
307307
- name: Export OpenAPI documents
308308
run: |
@@ -593,9 +593,9 @@ jobs:
593593
env:
594594
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
595595
run: |
596-
composer global require --prefer-dist --no-interaction --no-progress --ansi cedx/coveralls
596+
composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
597597
export PATH="$PATH:$HOME/.composer/vendor/bin"
598-
coveralls build/logs/behat/clover.xml
598+
php-coveralls --coverage_clover=build/logs/behat/clover.xml
599599
continue-on-error: true
600600

601601
elasticsearch:
@@ -1117,8 +1117,8 @@ jobs:
11171117
- name: Run Behat tests
11181118
run: |
11191119
mkdir -p build/logs/behat
1120-
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@php8'
1121-
- name: Run Behat tests
1120+
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@php8' --tags='~@v3'
1121+
- name: Run Behat tests (PHP 8)
11221122
if: (startsWith(matrix.php, '8.'))
11231123
run: |
11241124
mkdir -p build/logs/behat

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@
1111
/swagger.yaml
1212
/tests/Fixtures/app/var/
1313
/tests/Fixtures/app/public/bundles/
14+
/tests/Metadata/Extractor/Adapter/*.xml
15+
/tests/Metadata/Extractor/Adapter/*.yaml
1416
/vendor/
1517
/Dockerfile

behat.yml.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,3 @@ elasticsearch-coverage:
142142
- 'ApiPlatform\Core\Tests\Behat\CoverageContext'
143143
- 'Behat\MinkExtension\Context\MinkContext'
144144
- 'behatch:context:rest'
145-

features/main/crud.feature

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# todo remove @v3 tag in 3.0
12
Feature: Create-Retrieve-Update-Delete
23
In order to use an hypermedia API
34
As a client software developer
@@ -649,3 +650,129 @@ Feature: Create-Retrieve-Update-Delete
649650
"foo": "bar"
650651
}
651652
"""
653+
654+
@v3
655+
Scenario: Get a resource in v3 configured in YAML
656+
Given there is a Program
657+
When I send a "GET" request to "/programs/1"
658+
Then the response status code should be 200
659+
And the response should be in JSON
660+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
661+
And the JSON should be equal to:
662+
"""
663+
{
664+
"@context": "/contexts/Program",
665+
"@id": "/programs/1",
666+
"@type": "Program",
667+
"id": 1,
668+
"name": "Lorem ipsum 1",
669+
"date": "2015-03-01T10:00:00+00:00",
670+
"author": "/users/1"
671+
}
672+
"""
673+
674+
@v3
675+
Scenario: Get a collection resource in v3 configured in YAML
676+
Given there are 3 Programs
677+
When I send a "GET" request to "/users/1/programs"
678+
Then the response status code should be 200
679+
And the response should be in JSON
680+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
681+
And the JSON should be equal to:
682+
"""
683+
{
684+
"@context": "/contexts/Program",
685+
"@id": "/users/1/programs",
686+
"@type": "hydra:Collection",
687+
"hydra:member": [
688+
{
689+
"@id": "/programs/1",
690+
"@type": "Program",
691+
"id": 1,
692+
"name": "Lorem ipsum 1",
693+
"date": "2015-03-01T10:00:00+00:00",
694+
"author": "/users/1"
695+
},
696+
{
697+
"@id": "/programs/2",
698+
"@type": "Program",
699+
"id": 2,
700+
"name": "Lorem ipsum 2",
701+
"date": "2015-03-02T10:00:00+00:00",
702+
"author": "/users/1"
703+
},
704+
{
705+
"@id": "/programs/3",
706+
"@type": "Program",
707+
"id": 3,
708+
"name": "Lorem ipsum 3",
709+
"date": "2015-03-03T10:00:00+00:00",
710+
"author": "/users/1"
711+
}
712+
],
713+
"hydra:totalItems": 3
714+
}
715+
"""
716+
717+
@v3
718+
Scenario: Get a resource in v3 configured in XML
719+
Given there is a Comment
720+
When I send a "GET" request to "/comments/1"
721+
Then the response status code should be 200
722+
And the response should be in JSON
723+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
724+
And the JSON should be equal to:
725+
"""
726+
{
727+
"@context": "/contexts/Comment",
728+
"@id": "/comments/1",
729+
"@type": "Comment",
730+
"id": 1,
731+
"comment": "Lorem ipsum dolor sit amet 1",
732+
"date": "2015-03-01T10:00:00+00:00",
733+
"author": "/users/1"
734+
}
735+
"""
736+
737+
@v3
738+
Scenario: Get a collection resource in v3 configured in XML
739+
Given there are 3 Comments
740+
When I send a "GET" request to "/users/1/comments"
741+
Then the response status code should be 200
742+
And the response should be in JSON
743+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
744+
And the JSON should be equal to:
745+
"""
746+
{
747+
"@context": "/contexts/Comment",
748+
"@id": "/users/1/comments",
749+
"@type": "hydra:Collection",
750+
"hydra:member": [
751+
{
752+
"@id": "/comments/1",
753+
"@type": "Comment",
754+
"id": 1,
755+
"comment": "Lorem ipsum dolor sit amet 1",
756+
"date": "2015-03-01T10:00:00+00:00",
757+
"author": "/users/1"
758+
},
759+
{
760+
"@id": "/comments/2",
761+
"@type": "Comment",
762+
"id": 2,
763+
"comment": "Lorem ipsum dolor sit amet 2",
764+
"date": "2015-03-02T10:00:00+00:00",
765+
"author": "/users/1"
766+
},
767+
{
768+
"@id": "/comments/3",
769+
"@type": "Comment",
770+
"id": 3,
771+
"comment": "Lorem ipsum dolor sit amet 3",
772+
"date": "2015-03-03T10:00:00+00:00",
773+
"author": "/users/1"
774+
}
775+
],
776+
"hydra:totalItems": 3
777+
}
778+
"""

features/main/table_inheritance.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ Feature: Table inheritance
445445
}
446446
"""
447447

448-
Scenario: Get the parent interface collection
448+
Scenario: Get the parent interface collection
449449
When I send a "GET" request to "/resource_interfaces"
450450
Then the response status code should be 200
451451
And the response should be in JSON

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ parameters:
3838
- tests/Symfony/Bundle/Test/WebTestCaseTest.php
3939
- tests/Core/ProphecyTrait.php
4040
- tests/Core/Behat/CoverageContext.php
41+
- tests/Core/Behat/DoctrineContext.php
4142
- tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php
4243
# Templates for Maker
4344
- src/Core/Bridge/Symfony/Maker/Resources/skeleton

src/Core/Bridge/Symfony/Routing/RouteNameGenerator.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,19 @@ public static function generate(string $operationName, string $resourceShortName
4646
throw new InvalidArgumentException('Subresource operations are not supported by the RouteNameGenerator.');
4747
}
4848

49-
return sprintf(
50-
'%s%s_%s_%s',
49+
$operationName = sprintf(
50+
'%s%s_%s',
5151
static::ROUTE_NAME_PREFIX,
5252
self::inflector($resourceShortName),
53-
$operationName,
54-
$operationType
53+
$operationName
5554
);
55+
56+
// prevent api_foo_get_collection_collection
57+
if ("_$operationType" !== substr($operationName, -\strlen("_$operationType"))) {
58+
$operationName .= "_$operationType";
59+
}
60+
61+
return $operationName;
5662
}
5763

5864
/**

src/Core/Metadata/Extractor/XmlExtractor.php

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\Metadata\Extractor;
1515

1616
use ApiPlatform\Exception\InvalidArgumentException;
17+
use ApiPlatform\Metadata\Extractor\AbstractResourceExtractor;
18+
use ApiPlatform\Metadata\Extractor\PropertyExtractorInterface;
1719
use Symfony\Component\Config\Util\XmlUtils;
1820

1921
/**
@@ -22,11 +24,33 @@
2224
* @author Kévin Dunglas <[email protected]>
2325
* @author Antoine Bluchet <[email protected]>
2426
* @author Baptiste Meyer <[email protected]>
27+
* @author Vincent Chalamon <[email protected]>
28+
*
29+
* @deprecated since 2.7, to remove in 3.0 (replaced by ApiPlatform\Metadata\Extractor\XmlExtractor)
2530
*/
26-
final class XmlExtractor extends AbstractExtractor
31+
final class XmlExtractor extends AbstractResourceExtractor implements PropertyExtractorInterface
2732
{
2833
public const RESOURCE_SCHEMA = __DIR__.'/../schema/metadata.xsd';
2934

35+
private $properties;
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getProperties(): array
41+
{
42+
if (null !== $this->properties) {
43+
return $this->properties;
44+
}
45+
46+
$this->properties = [];
47+
foreach ($this->paths as $path) {
48+
$this->extractPath($path);
49+
}
50+
51+
return $this->properties;
52+
}
53+
3054
/**
3155
* {@inheritdoc}
3256
*/
@@ -46,23 +70,24 @@ protected function extractPath(string $path)
4670
'shortName' => $this->phpizeAttribute($resource, 'shortName', 'string'),
4771
'description' => $this->phpizeAttribute($resource, 'description', 'string'),
4872
'iri' => $this->phpizeAttribute($resource, 'iri', 'string'),
49-
'itemOperations' => $this->getOperations($resource, 'itemOperation'),
50-
'collectionOperations' => $this->getOperations($resource, 'collectionOperation'),
51-
'subresourceOperations' => $this->getOperations($resource, 'subresourceOperation'),
52-
'graphql' => $this->getOperations($resource, 'operation'),
53-
'attributes' => $this->getAttributes($resource, 'attribute') ?: null,
54-
'properties' => $this->getProperties($resource) ?: null,
73+
'itemOperations' => $this->extractOperations($resource, 'itemOperation'),
74+
'collectionOperations' => $this->extractOperations($resource, 'collectionOperation'),
75+
'subresourceOperations' => $this->extractOperations($resource, 'subresourceOperation'),
76+
'graphql' => $this->extractOperations($resource, 'operation'),
77+
'attributes' => $this->extractAttributes($resource, 'attribute') ?: null,
78+
'properties' => $this->extractProperties($resource) ?: null,
5579
];
80+
$this->properties[$resourceClass] = $this->resources[$resourceClass]['properties'];
5681
}
5782
}
5883

5984
/**
6085
* Returns the array containing configured operations. Returns NULL if there is no operation configuration.
6186
*/
62-
private function getOperations(\SimpleXMLElement $resource, string $operationType): ?array
87+
private function extractOperations(\SimpleXMLElement $resource, string $operationType): ?array
6388
{
6489
$graphql = 'operation' === $operationType;
65-
if (!$graphql && $legacyOperations = $this->getAttributes($resource, $operationType)) {
90+
if (!$graphql && $legacyOperations = $this->extractAttributes($resource, $operationType)) {
6691
@trigger_error(
6792
sprintf('Configuring "%1$s" tags without using a parent "%1$ss" tag is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', $operationType),
6893
\E_USER_DEPRECATED
@@ -76,17 +101,17 @@ private function getOperations(\SimpleXMLElement $resource, string $operationTyp
76101
return null;
77102
}
78103

79-
return $this->getAttributes($resource->{$operationsParent}, $operationType, true);
104+
return $this->extractAttributes($resource->{$operationsParent}, $operationType, true);
80105
}
81106

82107
/**
83108
* Recursively transforms an attribute structure into an associative array.
84109
*/
85-
private function getAttributes(\SimpleXMLElement $resource, string $elementName, bool $topLevel = false): array
110+
private function extractAttributes(\SimpleXMLElement $resource, string $elementName, bool $topLevel = false): array
86111
{
87112
$attributes = [];
88113
foreach ($resource->{$elementName} as $attribute) {
89-
$value = isset($attribute->attribute[0]) ? $this->getAttributes($attribute, 'attribute') : $this->phpizeContent($attribute);
114+
$value = isset($attribute->attribute[0]) ? $this->extractAttributes($attribute, 'attribute') : $this->phpizeContent($attribute);
90115
// allow empty operations definition, like <collectionOperation name="post" />
91116
if ($topLevel && '' === $value) {
92117
$value = [];
@@ -104,7 +129,7 @@ private function getAttributes(\SimpleXMLElement $resource, string $elementName,
104129
/**
105130
* Gets metadata of a property.
106131
*/
107-
private function getProperties(\SimpleXMLElement $resource): array
132+
private function extractProperties(\SimpleXMLElement $resource): array
108133
{
109134
$properties = [];
110135
foreach ($resource->property as $property) {
@@ -117,7 +142,7 @@ private function getProperties(\SimpleXMLElement $resource): array
117142
'required' => $this->phpizeAttribute($property, 'required', 'bool'),
118143
'identifier' => $this->phpizeAttribute($property, 'identifier', 'bool'),
119144
'iri' => $this->phpizeAttribute($property, 'iri', 'string'),
120-
'attributes' => $this->getAttributes($property, 'attribute'),
145+
'attributes' => $this->extractAttributes($property, 'attribute'),
121146
'subresource' => $property->subresource ? [
122147
'collection' => $this->phpizeAttribute($property->subresource, 'collection', 'bool'),
123148
'resourceClass' => $this->resolve($this->phpizeAttribute($property->subresource, 'resourceClass', 'string')),

0 commit comments

Comments
 (0)