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!