diff --git a/composer.json b/composer.json index f760f5c..41947b4 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.1", + "opis/json-schema": "^2.3", "phpdocumentor/reflection-docblock": "^5.4", "symfony/asset": "^7.0", "symfony/console": "^7.0", diff --git a/composer.lock b/composer.lock index a3c72ad..1ee82c1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e1d68fda857d3a6a99a9c9d4e0c1c4dd", + "content-hash": "15539a85e57277b17ba1f864a4f4d0ee", "packages": [ { "name": "composer/package-versions-deprecated", @@ -174,16 +174,16 @@ }, { "name": "doctrine/collections", - "version": "2.2.1", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "420480fc085bc65f3c956af13abe8e7546f94813" + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/420480fc085bc65f3c956af13abe8e7546f94813", - "reference": "420480fc085bc65f3c956af13abe8e7546f94813", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", "shasum": "" }, "require": { @@ -240,7 +240,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.2.1" + "source": "https://github.com/doctrine/collections/tree/2.2.2" }, "funding": [ { @@ -256,7 +256,7 @@ "type": "tidelift" } ], - "time": "2024-03-05T22:28:45+00:00" + "time": "2024-04-18T06:56:21+00:00" }, { "name": "doctrine/dbal", @@ -1057,16 +1057,16 @@ }, { "name": "doctrine/orm", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "9c560713925ac5859342e6ff370c4c997acf2fd4" + "reference": "f79d166a4e844beb9389f23bdb44abdbf58cec38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/9c560713925ac5859342e6ff370c4c997acf2fd4", - "reference": "9c560713925ac5859342e6ff370c4c997acf2fd4", + "url": "https://api.github.com/repos/doctrine/orm/zipball/f79d166a4e844beb9389f23bdb44abdbf58cec38", + "reference": "f79d166a4e844beb9389f23bdb44abdbf58cec38", "shasum": "" }, "require": { @@ -1139,9 +1139,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.1.1" + "source": "https://github.com/doctrine/orm/tree/3.1.2" }, - "time": "2024-03-21T11:37:52+00:00" + "time": "2024-04-15T14:20:40+00:00" }, { "name": "doctrine/persistence", @@ -1606,6 +1606,196 @@ ], "time": "2024-04-12T21:02:21+00:00" }, + { + "name": "opis/json-schema", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.0", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.3.0" + }, + "time": "2022-01-08T20:38:03+00:00" + }, + { + "name": "opis/string", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.0.1" + }, + "time": "2022-01-14T15:42:23+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -7112,34 +7302,34 @@ }, { "name": "twig/extra-bundle", - "version": "v3.8.0", + "version": "v3.9.3", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140" + "reference": "ef6869adf1fdab66f7e495771a7ba01496ffc0d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/32807183753de0388c8e59f7ac2d13bb47311140", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/ef6869adf1fdab66f7e495771a7ba01496ffc0d5", + "reference": "ef6869adf1fdab66f7e495771a7ba01496ffc0d5", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" }, "type": "symfony-bundle", "autoload": { @@ -7170,7 +7360,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.8.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.9.3" }, "funding": [ { @@ -7182,34 +7372,41 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-04-18T09:24:21+00:00" }, { "name": "twig/twig", - "version": "v3.8.0", + "version": "v3.9.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58", + "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-php80": "^1.22" }, "require-dev": { "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -7242,7 +7439,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/v3.9.3" }, "funding": [ { @@ -7254,7 +7451,7 @@ "type": "tidelift" } ], - "time": "2023-11-21T18:54:41+00:00" + "time": "2024-04-18T11:59:33+00:00" }, { "name": "webmozart/assert", diff --git a/jsonSchema/createDinosaur.json b/jsonSchema/createDinosaur.json new file mode 100644 index 0000000..d3a8e9e --- /dev/null +++ b/jsonSchema/createDinosaur.json @@ -0,0 +1,60 @@ +{ + "type": "object", + "title": "Dinausor create", + "description": "Input data for creating a new dinausor", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name of the dinausor", + "maxLength": 255, + "exemples": [ + "Aladar" + ] + }, + "gender": { + "type": "string", + "title": "Gender", + "description": "Gender of the dinausor", + "maxLength": 255, + "pattern": "Male|Female", + "exemples": [ + "Male", + "Female" + ] + }, + "speciesId": { + "type": "integer", + "title": "Species ID", + "description": "Species of the dinausor", + "exclusiveMinimum": 0, + "exemples": [ + 0, + 1, + 2 + ] + }, + "age": { + "type": "integer", + "title": "Age", + "description": "Age of the dinausor", + "exclusiveMinimum": 0, + "exemples": [ + 56, + 78, + 80 + ] + }, + "eyesColor": { + "type": "string", + "title": "Eye color", + "description": "Eye color of the dinausor", + "maxLength": 255, + "exemples": [ + "brown", + "blue", + "green" + ] + } + } +} diff --git a/src/Controller/API/Dinosaurs/Create.php b/src/Controller/API/Dinosaurs/Create.php index 61f2249..fe7ec41 100644 --- a/src/Controller/API/Dinosaurs/Create.php +++ b/src/Controller/API/Dinosaurs/Create.php @@ -6,8 +6,11 @@ use App\Entity\Dinosaur; use App\Entity\Species; +use App\Validator\JsonSchema\Validator as JsonSchemaValidator; +use App\Validator\JsonSchema\ValidationException as JsonSchemaValidationException; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -17,15 +20,33 @@ final class Create extends AbstractController { public function __construct( - private readonly SerializerInterface $serializer + private readonly SerializerInterface $serializer, + #[Autowire('%kernel.project_dir%/jsonSchema')] + private readonly string $jsonSchemaDir ) { } #[Route('/api/dinosaurs', methods: 'POST')] - public function __invoke(ManagerRegistry $manager, Request $request): Response - { + public function __invoke( + ManagerRegistry $manager, + Request $request, + JsonSchemaValidator $validator + ): Response { $dinosaurData = json_decode($request->getContent(), true); + try { + $validator->validate( + $dinosaurData, + $this->jsonSchemaDir . '/createDinosaur.json' + ); + } catch (JsonSchemaValidationException $e) { + return new JsonResponse( + $e->getMessage(), + Response::HTTP_BAD_REQUEST, + json: true + ); + } + $species = $manager ->getRepository(Species::class) ->find($dinosaurData['speciesId']); diff --git a/src/Validator/JsonSchema/Error.php b/src/Validator/JsonSchema/Error.php new file mode 100644 index 0000000..b297252 --- /dev/null +++ b/src/Validator/JsonSchema/Error.php @@ -0,0 +1,24 @@ +path; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Validator/JsonSchema/Errors.php b/src/Validator/JsonSchema/Errors.php new file mode 100644 index 0000000..3171e24 --- /dev/null +++ b/src/Validator/JsonSchema/Errors.php @@ -0,0 +1,34 @@ + + */ +final readonly class Errors implements \Countable, \IteratorAggregate +{ + /** + * @var array + */ + private array $errors; + + public function __construct(Error ...$errors) + { + $this->errors = $errors; + } + + public function count(): int + { + return \count($this->errors); + } + + /** + * @return ArrayIterator + */ + public function getIterator(): \Iterator + { + return new \ArrayIterator($this->errors); + } +} diff --git a/src/Validator/JsonSchema/ValidationException.php b/src/Validator/JsonSchema/ValidationException.php new file mode 100644 index 0000000..0bad005 --- /dev/null +++ b/src/Validator/JsonSchema/ValidationException.php @@ -0,0 +1,22 @@ +getPath()] = $error->getMessage(); + } + + parent::__construct(json_encode($data, JSON_THROW_ON_ERROR)); + } +} diff --git a/src/Validator/JsonSchema/Validator.php b/src/Validator/JsonSchema/Validator.php new file mode 100644 index 0000000..f0e4fcf --- /dev/null +++ b/src/Validator/JsonSchema/Validator.php @@ -0,0 +1,75 @@ +validator = new JsonSchemaValidator(); + } + + /** + * @param mixed $data + * @param class-string|JsonSchema $schema + */ + public function validate($data, $schemaPath): void + { + $data = json_decode( + json_encode( + $data, + JSON_THROW_ON_ERROR + ), + flags: JSON_THROW_ON_ERROR + ); + + $schema = json_decode( + json_encode( + \file_get_contents($schemaPath), + JSON_THROW_ON_ERROR + ), + flags: JSON_THROW_ON_ERROR + ); + + $result = $this->validator->validate($data, $schema); + + if ($result->isValid()) { + return; + } + + throw new ValidationException( + new Errors( + ...$this->yieldErrors($result->error()) + ) + ); + } + + /** + * @return iterable + */ + private function yieldErrors(ValidationError $error): iterable + { + $formatter = new ErrorFormatter(); + + $errors = $formatter->format($error); + + foreach ($errors as $field => $message) { + $formatted = sizeof($message) === 1 + ? $message[0] + : json_encode($message); + + yield new Error( + $field, + $formatted, + ); + } + } +}