diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a0e8ef72884..3d284513e54 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -70,7 +70,7 @@ jobs:
run: composer require symfony/uid --dev --no-interaction --no-progress --ansi
- name: Install PHPUnit
env:
- SYMFONY_PHPUNIT_VERSION: '9.4'
+ SYMFONY_PHPUNIT_VERSION: '9.5'
run: vendor/bin/simple-phpunit --version
- name: Cache PHPStan results
uses: actions/cache@v2
@@ -86,7 +86,7 @@ jobs:
tests/Fixtures/app/console cache:clear --ansi
- name: Run PHPStan analysis
env:
- SYMFONY_PHPUNIT_VERSION: '9.4'
+ SYMFONY_PHPUNIT_VERSION: '9.5'
run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --no-interaction --ansi
phpunit:
@@ -102,7 +102,7 @@ jobs:
- '7.4'
- '8.0'
include:
- - php: '7.4'
+ - php: '8.0'
coverage: true
fail-fast: false
steps:
@@ -197,7 +197,7 @@ jobs:
- '7.4'
- '8.0'
include:
- - php: '7.4'
+ - php: '8.0'
coverage: true
fail-fast: false
steps:
diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php
similarity index 97%
rename from .php_cs.dist
rename to .php-cs-fixer.dist.php
index 5c91b2b833c..1f1d74ce2c6 100644
--- a/.php_cs.dist
+++ b/.php-cs-fixer.dist.php
@@ -26,7 +26,7 @@
'tests/Fixtures/app/console',
]);
-return PhpCsFixer\Config::create()
+return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@DoctrineAnnotation' => true,
@@ -41,9 +41,6 @@
'comment_type' => 'phpdocs_like',
],
'array_indentation' => true,
- 'array_syntax' => [
- 'syntax' => 'short',
- ],
'compact_nullable_typehint' => true,
'doctrine_annotation_array_assignment' => [
'operator' => '=',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9462b67670a..b81907cea9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
* Filter validation: Fix issue in Required filter validator with dot notation (#4221)
* OpenAPI: Fix notice/warning for `response` without `content` in the `openapi_context` (#4210)
* OpenAPI: Do not use output for request body (#4213)
+* OpenAPI: Do not use jsonld schema for all media types (#4247) (BC note: `SchemaFactory::buildSchema()` is now immutable as it no longer modifies the passed `$schema`)
* Serializer: Convert internal error to HTTP 400 in Ramsey uuid denormalization from invalid body string (#4200)
* Symfony: Add tests with Symfony Uuid (#4230)
* OpenAPI: Allow to set extensionProperties with YAML schema definition (#4228)
diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature
index 3f492d6f36c..6a77250474e 100644
--- a/features/openapi/docs.feature
+++ b/features/openapi/docs.feature
@@ -109,6 +109,103 @@ Feature: Documentation support
And the JSON node "paths./deprecated_resources/{id}.put.deprecated" should be true
And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true
+ # Formats
+ And the OpenAPI class "Dummy.jsonld" exists
+ And the "@id" property exists for the OpenAPI class "Dummy.jsonld"
+ And the JSON node "paths./dummies.get.responses.200.content.application/ld+json" should be equal to:
+ """
+ {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "hydra:member": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Dummy.jsonld"
+ }
+ },
+ "hydra:totalItems": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {
+ "type": "string",
+ "format": "iri-reference"
+ },
+ "@type": {
+ "type": "string"
+ },
+ "hydra:first": {
+ "type": "string",
+ "format": "iri-reference"
+ },
+ "hydra:last": {
+ "type": "string",
+ "format": "iri-reference"
+ },
+ "hydra:next": {
+ "type": "string",
+ "format": "iri-reference"
+ }
+ }
+ },
+ "hydra:search": {
+ "type": "object",
+ "properties": {
+ "@type": {
+ "type": "string"
+ },
+ "hydra:template": {
+ "type": "string"
+ },
+ "hydra:variableRepresentation": {
+ "type": "string"
+ },
+ "hydra:mapping": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "@type": {
+ "type": "string"
+ },
+ "variable": {
+ "type": "string"
+ },
+ "property": {
+ "type": "string",
+ "nullable": true
+ },
+ "required": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "hydra:member"
+ ]
+ }
+ }
+ """
+ And the JSON node "paths./dummies.get.responses.200.content.application/json" should be equal to:
+ """
+ {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Dummy"
+ }
+ }
+ }
+ """
+
@createSchema
Scenario: Retrieve the Swagger documentation
Given I send a "GET" request to "/docs.json?spec_version=2"
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index f9a17a2f633..7ce4570a647 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -35,6 +35,7 @@ parameters:
- tests/Bridge/Symfony/Bundle/Test/WebTestCaseTest.php
- tests/ProphecyTrait.php
- tests/Behat/CoverageContext.php
+ - tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php
earlyTerminatingMethodCalls:
PHPUnit\Framework\Constraint\Constraint:
- fail
@@ -89,6 +90,8 @@ parameters:
# Expected, Symfony 5.3
- '#getCollectionValueT|getCollectionKeyTyp#'
+ - '#isPasswordValid#'
+ - '#UserPasswordHasherInterface#'
# Expected, due to PHP 8 attributes
- '#ReflectionProperty::getAttributes\(\)#'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 24793a48189..6c8892bd6b5 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -29,7 +29,9 @@
vendor
src/Bridge/NelmioApiDoc
src/Bridge/FosUser
+ .php-cs-fixer.dist.php
src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+ src/Bridge/Symfony/Bundle/Test/Constraint/ArraySubsetLegacy.php
diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php
index b518c308c1b..3af9edbd586 100644
--- a/src/JsonSchema/SchemaFactory.php
+++ b/src/JsonSchema/SchemaFactory.php
@@ -69,7 +69,7 @@ public function addDistinctFormat(string $format): void
*/
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
- $schema = $schema ?? new Schema();
+ $schema = $schema ? clone $schema : new Schema();
if (null === $metadata = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext)) {
return $schema;
}
diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php
index e126e094b0e..c976f011f25 100644
--- a/src/OpenApi/Factory/OpenApiFactory.php
+++ b/src/OpenApi/Factory/OpenApiFactory.php
@@ -254,9 +254,6 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
}
}
- $schema = new Schema('openapi');
- $schema->setDefinitions($schemas);
-
$requestBody = null;
if ($contextRequestBody = $operation['openapi_context']['requestBody'] ?? false) {
$requestBody = new Model\RequestBody($contextRequestBody['description'] ?? '', new \ArrayObject($contextRequestBody['content']), $contextRequestBody['required'] ?? false);
diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php
index 9ce098bec77..b0a1db323ed 100644
--- a/src/Swagger/Serializer/DocumentationNormalizer.php
+++ b/src/Swagger/Serializer/DocumentationNormalizer.php
@@ -637,9 +637,7 @@ private function getJsonSchema(bool $v3, \ArrayObject $definitions, string $reso
$schema = new Schema($v3 ? Schema::VERSION_OPENAPI : Schema::VERSION_SWAGGER);
$schema->setDefinitions($definitions);
- $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
-
- return $schema;
+ return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
}
private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php
index ba3c614b24a..d14881f4c4b 100644
--- a/tests/Behat/DoctrineContext.php
+++ b/tests/Behat/DoctrineContext.php
@@ -172,6 +172,7 @@
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Ramsey\Uuid\Uuid;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Uid\Uuid as SymfonyUuid;
@@ -185,7 +186,7 @@ final class DoctrineContext implements Context
*/
private $manager;
private $doctrine;
- private $passwordEncoder;
+ private $passwordHasher;
private $schemaTool;
private $schemaManager;
@@ -195,11 +196,13 @@ final class DoctrineContext implements Context
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
+ *
+ * @param UserPasswordEncoderInterface|UserPasswordHasherInterface $passwordHasher
*/
- public function __construct(ManagerRegistry $doctrine, UserPasswordEncoderInterface $passwordEncoder)
+ public function __construct(ManagerRegistry $doctrine, $passwordHasher)
{
$this->doctrine = $doctrine;
- $this->passwordEncoder = $passwordEncoder;
+ $this->passwordHasher = $passwordHasher;
$this->manager = $doctrine->getManager();
$this->schemaTool = $this->manager instanceof EntityManagerInterface ? new SchemaTool($this->manager) : null;
$this->schemaManager = $this->manager instanceof DocumentManager ? $this->manager->getSchemaManager() : null;
@@ -1282,7 +1285,7 @@ public function thereIsAUrlEncodedIdResource()
public function thePasswordForUserShouldBeHashed(string $password, string $user)
{
$user = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find($user);
- if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
+ if (!$this->passwordHasher->isPasswordValid($user, $password)) {
throw new \Exception('User password mismatch');
}
}
diff --git a/tests/Fixtures/TestBundle/Document/User.php b/tests/Fixtures/TestBundle/Document/User.php
index 34e04a79762..4a19447c268 100644
--- a/tests/Fixtures/TestBundle/Document/User.php
+++ b/tests/Fixtures/TestBundle/Document/User.php
@@ -18,8 +18,8 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Security\AbstractSecurityUser;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
-use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
/**
@@ -58,7 +58,7 @@
* @author Théo FIDRY
* @author Kévin Dunglas
*/
-class User implements UserInterface
+class User extends AbstractSecurityUser
{
/**
* @var int
@@ -156,7 +156,7 @@ public function getRoles(): array
return ['ROLE_USER'];
}
- public function getPassword()
+ public function getPassword(): ?string
{
return null;
}
diff --git a/tests/Fixtures/TestBundle/Entity/User.php b/tests/Fixtures/TestBundle/Entity/User.php
index 37653366199..616b9d4a8d0 100644
--- a/tests/Fixtures/TestBundle/Entity/User.php
+++ b/tests/Fixtures/TestBundle/Entity/User.php
@@ -18,8 +18,8 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Security\AbstractSecurityUser;
use Doctrine\ORM\Mapping as ORM;
-use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
/**
@@ -59,7 +59,7 @@
* @author Théo FIDRY
* @author Kévin Dunglas
*/
-class User implements UserInterface
+class User extends AbstractSecurityUser
{
/**
* @var int
@@ -159,7 +159,7 @@ public function getRoles(): array
return ['ROLE_USER'];
}
- public function getPassword()
+ public function getPassword(): ?string
{
return null;
}
diff --git a/tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php b/tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php
new file mode 100644
index 00000000000..5a649689fbb
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Security;
+
+use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
+use Symfony\Component\Security\Core\User\UserInterface;
+
+// Forward compatibility layer for symfony/password-hasher:^5.3
+if (interface_exists(PasswordAuthenticatedUserInterface::class)) {
+ abstract class AbstractSecurityUser implements UserInterface, PasswordAuthenticatedUserInterface
+ {
+ abstract public function getPassword(): ?string;
+
+ public function getUserIdentifier(): string
+ {
+ return $this->getUsername();
+ }
+ }
+} else {
+ abstract class AbstractSecurityUser implements UserInterface
+ {
+ }
+}
diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php
index 83fc3aed777..b5ac1c4bb98 100644
--- a/tests/Fixtures/app/AppKernel.php
+++ b/tests/Fixtures/app/AppKernel.php
@@ -12,6 +12,7 @@
declare(strict_types=1);
use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle;
+use ApiPlatform\Core\Tests\Behat\DoctrineContext;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\TestBundle;
@@ -117,6 +118,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
$loader->load(__DIR__.'/config/config_symfony_uid.yml');
}
+ $c->getDefinition(DoctrineContext::class)->setArgument('$passwordHasher', class_exists(NativePasswordHasher::class) ? 'security.user_password_encoder' : 'security.user_password_hasher');
+
$c->prependExtensionConfig('framework', [
'secret' => 'dunglas.fr',
'validation' => ['enable_annotations' => true],
@@ -136,9 +139,9 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
'router' => ['utf8' => true],
]);
- $alg = class_exists(NativePasswordHasher::class) || class_exists('Symfony\Component\Security\Core\Encoder\NativePasswordEncoder') ? 'auto' : 'bcrypt';
+ $alg = class_exists(NativePasswordHasher::class, false) || class_exists('Symfony\Component\Security\Core\Encoder\NativePasswordEncoder') ? 'auto' : 'bcrypt';
$securityConfig = [
- 'encoders' => [
+ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [
User::class => $alg,
UserDocument::class => $alg,
// Don't use plaintext in production!