diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf6be6..5db9953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0 - **`BridgeBundleInfo` value object** carrying the bundle's name + class FQCN. `HealthController` now constructor-injects this instead of `PublisherInterface` as the deep-load canary, so the readiness probe is no longer coupled to the publisher's contract. `/healthz` response gains a `name` field (`php-qml/bridge`); the `bundle` field now reports `PhpQml\Bridge\BridgeBundle` (was `PhpQml\Bridge\Publisher`). - **`Maker\Support\NameInput`** — shared interactive name prompt. All three `make:bridge:*` makers (`resource`, `command`, `window`) re-implemented the same "prompt, trim, ucfirst, reject empty" closure inline; collapsed into one call site so empty-argument and validation behaviour stay in lockstep. - **`Maker\Support\Naming`** — `camelTo($name, $separator)` helper. Replaces inline `preg_replace('/(?Dto` + `UpdateDto` under `src/Dto/` alongside the controller, and the controller dispatches via `#[MapRequestPayload]`. Closes the input-validation gap from the audit: malformed JSON, missing required fields, or `#[Assert\NotBlank]` violations now produce RFC 7807 `application/problem+json` automatically (Symfony's `RequestPayloadValueResolver`) — no more `if (isset($data['title']))` boilerplate, no silent type coercion. Update DTOs use nullable defaults so PATCH callers send only the fields they want changed. Without `--with-dto` the legacy template still ships unchanged. Maker fails loud if `symfony/validator` isn't autoloadable. Skeleton + example/todo composer.json pull `symfony/validator` so scaffolded apps work out of the box. Snapshot test exercises both modes. ### Changed diff --git a/examples/todo/symfony/composer.json b/examples/todo/symfony/composer.json index 09930f0..2a7334b 100644 --- a/examples/todo/symfony/composer.json +++ b/examples/todo/symfony/composer.json @@ -11,6 +11,7 @@ "symfony/security-bundle": "^8.0", "symfony/mercure-bundle": "^0.4", "symfony/uid": "^8.0", + "symfony/validator": "^8.0", "doctrine/orm": "^3.0", "doctrine/doctrine-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^4.0", diff --git a/examples/todo/symfony/composer.lock b/examples/todo/symfony/composer.lock index 4fab7d2..70e5b92 100644 --- a/examples/todo/symfony/composer.lock +++ b/examples/todo/symfony/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": "50ef8ab49885db8d3709edc1c8e68e05", + "content-hash": "d001a6d1e30f94b4b5044262009031fc", "packages": [ { "name": "doctrine/collections", @@ -1199,13 +1199,13 @@ "dist": { "type": "path", "url": "../../../framework/php", - "reference": "68fca95525db2311a08deb931f1b92909b20c450" + "reference": "b426d4a8ca67cde4f3bd0471d340e348b1fd4053" }, "require": { "doctrine/dbal": "^4.0", "doctrine/doctrine-bundle": "^3.0", "doctrine/orm": "^3.0", - "php": "^8.3", + "php": "^8.4", "symfony/config": "^8.0", "symfony/console": "^8.0", "symfony/dependency-injection": "^8.0", @@ -1259,7 +1259,7 @@ ] }, "license": [ - "proprietary" + "LGPL-3.0-or-later" ], "description": "Symfony bundle bridging PHP applications to a Qt/QML host (part of the php-qml framework).", "transport-options": { @@ -5024,6 +5024,88 @@ ], "time": "2026-03-30T15:14:47+00:00" }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, { "name": "symfony/type-info", "version": "v8.0.9", @@ -5184,6 +5266,101 @@ ], "time": "2026-04-30T16:10:06+00:00" }, + { + "name": "symfony/validator", + "version": "v8.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "131dc8322c06595a6c98185787fa756deada20df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/131dc8322c06595a6c98185787fa756deada20df", + "reference": "131dc8322c06595a6c98185787fa756deada20df", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/doctrine-bridge": "<7.4", + "symfony/expression-language": "<7.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/type-info": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v8.0.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-30T16:10:06+00:00" + }, { "name": "symfony/var-dumper", "version": "v8.0.8", @@ -5743,7 +5920,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/framework/php/src/Maker/BridgeResourceMaker.php b/framework/php/src/Maker/BridgeResourceMaker.php index 7124034..a45eb97 100644 --- a/framework/php/src/Maker/BridgeResourceMaker.php +++ b/framework/php/src/Maker/BridgeResourceMaker.php @@ -24,6 +24,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Uid\Uuid; +use Symfony\Component\Validator\Constraints\NotBlank; /** * `make:bridge:resource ` — generates the three files needed to @@ -71,11 +72,21 @@ final class BridgeResourceMaker extends AbstractMaker InputOption::VALUE_NONE, 'Use auto-incrementing int IDs instead of the default UUIDv7.', ) + ->addOption( + 'with-dto', + null, + InputOption::VALUE_NONE, + 'Generate CreateDto + UpdateDto alongside the controller and dispatch via #[MapRequestPayload]. Requires symfony/validator.', + ) ->setHelp( - "The maker creates three files:\n\n" + "The maker creates three files (or five with --with-dto):\n\n" ." • src/Entity/Todo.php — Doctrine entity tagged with #[BridgeResource]\n" ." • src/Controller/TodoController.php — CRUD on /api/todos\n" ." • {qml_path}/TodoList.qml — starter ReactiveListModel snippet\n\n" + ."With --with-dto the controller dispatches via #[MapRequestPayload]\n" + ."against generated Create/Update DTOs (validated, no if-isset stubs):\n\n" + ." • src/Dto/CreateTodoDto.php — POST payload with #[Assert\\NotBlank] etc.\n" + ." • src/Dto/UpdateTodoDto.php — PATCH payload (all fields nullable)\n\n" ."After the maker, run bin/console make:migration and apply it.\n" ); } @@ -95,6 +106,13 @@ final class BridgeResourceMaker extends AbstractMaker { $rawName = (string) $input->getArgument('name'); $useUuid = !(bool) $input->getOption('int-id'); + $useDto = (bool) $input->getOption('with-dto'); + + if ($useDto && !class_exists(NotBlank::class)) { + $io->error('--with-dto requires symfony/validator. Run: composer require symfony/validator'); + + return; + } $singular = ucfirst(Str::asCamelCase($rawName)); $pluralCamel = Str::singularCamelCaseToPluralCamelCase($singular); @@ -128,9 +146,34 @@ final class BridgeResourceMaker extends AbstractMaker __DIR__.'/templates/Entity.tpl.php', $vars, ); + + if ($useDto) { + $createDto = $generator->createClassNameDetails( + 'Create'.$singular, + 'Dto\\', + 'Dto', + ); + $updateDto = $generator->createClassNameDetails( + 'Update'.$singular, + 'Dto\\', + 'Dto', + ); + $generator->generateFile( + 'src/Dto/'.$createDto->getShortName().'.php', + __DIR__.'/templates/CreateDto.tpl.php', + $vars, + ); + $generator->generateFile( + 'src/Dto/'.$updateDto->getShortName().'.php', + __DIR__.'/templates/UpdateDto.tpl.php', + $vars, + ); + } + + $controllerTemplate = $useDto ? 'ControllerWithDto.tpl.php' : 'Controller.tpl.php'; $generator->generateFile( 'src/Controller/'.$controllerFqcn->getShortName().'.php', - __DIR__.'/templates/Controller.tpl.php', + __DIR__.'/templates/'.$controllerTemplate, $vars, ); diff --git a/framework/php/src/Maker/templates/ControllerWithDto.tpl.php b/framework/php/src/Maker/templates/ControllerWithDto.tpl.php new file mode 100644 index 0000000..9e9c988 --- /dev/null +++ b/framework/php/src/Maker/templates/ControllerWithDto.tpl.php @@ -0,0 +1,95 @@ + + +declare(strict_types=1); + +namespace App\Controller; + +use App\Dto\CreateDto; +use App\Dto\UpdateDto; +use ; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Auto-generated CRUD controller for the bridge resource (DTO-shaped). + * Edit freely — re-running make:bridge:resource won't overwrite this file. + * + * Validated input via #[MapRequestPayload]: malformed JSON, missing + * required fields, or constraint violations produce RFC 7807 + * problem+json automatically (Symfony's RequestPayloadValueResolver). + */ +#[Route('')] +final class Controller +{ + public function __construct( + private readonly EntityManagerInterface $em, + private readonly NormalizerInterface $normalizer, + ) { + } + + #[Route('', name: '_list', methods: ['GET'])] + public function list(): JsonResponse + { + $items = $this->em->getRepository(::class)->findAll(); + + return new JsonResponse($this->normalizer->normalize($items, 'json')); + } + + #[Route('', name: '_create', methods: ['POST'])] + public function create(#[MapRequestPayload] CreateDto $dto): JsonResponse + { + $entity = new (); + $entity->setTitle($dto->title); + $entity->setDone($dto->done); + + $this->em->persist($entity); + $this->em->flush(); + + return new JsonResponse( + $this->normalizer->normalize($entity, 'json'), + Response::HTTP_CREATED, + ); + } + + #[Route('/{id}', name: '_update', methods: ['PATCH'])] + public function update(string $id, #[MapRequestPayload] UpdateDto $dto): JsonResponse + { + $entity = $this->em->getRepository(::class)->find($id); + if (null === $entity) { + return new JsonResponse( + ['title' => 'Not Found', 'status' => 404], + Response::HTTP_NOT_FOUND, + ['Content-Type' => 'application/problem+json'], + ); + } + + if (null !== $dto->title) { + $entity->setTitle($dto->title); + } + if (null !== $dto->done) { + $entity->setDone($dto->done); + } + + $this->em->flush(); + + return new JsonResponse($this->normalizer->normalize($entity, 'json')); + } + + #[Route('/{id}', name: '_delete', methods: ['DELETE'])] + public function delete(string $id): JsonResponse + { + $entity = $this->em->getRepository(::class)->find($id); + if (null === $entity) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + $this->em->remove($entity); + $this->em->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/framework/php/src/Maker/templates/CreateDto.tpl.php b/framework/php/src/Maker/templates/CreateDto.tpl.php new file mode 100644 index 0000000..37082a2 --- /dev/null +++ b/framework/php/src/Maker/templates/CreateDto.tpl.php @@ -0,0 +1,26 @@ + + +declare(strict_types=1); + +namespace App\Dto; + +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Validated payload for POST . + * + * Auto-generated alongside Controller's create() action. + * #[MapRequestPayload] in the controller turns malformed JSON or any + * Assert violation here into an RFC 7807 problem+json response — no + * controller-level if-isset boilerplate, no silent type coercion. + */ +final readonly class CreateDto +{ + public function __construct( + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + public string $title, + public bool $done = false, + ) { + } +} diff --git a/framework/php/src/Maker/templates/UpdateDto.tpl.php b/framework/php/src/Maker/templates/UpdateDto.tpl.php new file mode 100644 index 0000000..794f73f --- /dev/null +++ b/framework/php/src/Maker/templates/UpdateDto.tpl.php @@ -0,0 +1,24 @@ + + +declare(strict_types=1); + +namespace App\Dto; + +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Validated payload for PATCH /{id}. + * + * All fields are nullable so PATCH callers can send only the fields + * they want to change. The controller checks each for null and + * skips the corresponding entity setter. + */ +final readonly class UpdateDto +{ + public function __construct( + #[Assert\Length(max: 255)] + public ?string $title = null, + public ?bool $done = null, + ) { + } +} diff --git a/framework/php/tests/snapshot/CreateTodoDto.php b/framework/php/tests/snapshot/CreateTodoDto.php new file mode 100644 index 0000000..de669b8 --- /dev/null +++ b/framework/php/tests/snapshot/CreateTodoDto.php @@ -0,0 +1,26 @@ +em->getRepository(Todo::class)->findAll(); + + return new JsonResponse($this->normalizer->normalize($items, 'json')); + } + + #[Route('', name: 'todo_create', methods: ['POST'])] + public function create(#[MapRequestPayload] CreateTodoDto $dto): JsonResponse + { + $entity = new Todo(); + $entity->setTitle($dto->title); + $entity->setDone($dto->done); + + $this->em->persist($entity); + $this->em->flush(); + + return new JsonResponse( + $this->normalizer->normalize($entity, 'json'), + Response::HTTP_CREATED, + ); + } + + #[Route('/{id}', name: 'todo_update', methods: ['PATCH'])] + public function update(string $id, #[MapRequestPayload] UpdateTodoDto $dto): JsonResponse + { + $entity = $this->em->getRepository(Todo::class)->find($id); + + if (null === $entity) { + return new JsonResponse( + ['title' => 'Not Found', 'status' => 404], + Response::HTTP_NOT_FOUND, + ['Content-Type' => 'application/problem+json'], + ); + } + + if (null !== $dto->title) { + $entity->setTitle($dto->title); + } + + if (null !== $dto->done) { + $entity->setDone($dto->done); + } + + $this->em->flush(); + + return new JsonResponse($this->normalizer->normalize($entity, 'json')); + } + + #[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])] + public function delete(string $id): JsonResponse + { + $entity = $this->em->getRepository(Todo::class)->find($id); + + if (null === $entity) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + $this->em->remove($entity); + $this->em->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/framework/php/tests/snapshot/UpdateTodoDto.php b/framework/php/tests/snapshot/UpdateTodoDto.php new file mode 100644 index 0000000..49ae464 --- /dev/null +++ b/framework/php/tests/snapshot/UpdateTodoDto.php @@ -0,0 +1,24 @@ +/dev/null \ - && bin/console make:bridge:command MarkAllDone --no-interaction >/dev/null \ - && bin/console make:bridge:window Todo --no-interaction >/dev/null ) - -# Compare each generated file to its snapshot baseline. fail=0 check() { local generated="$1" @@ -49,15 +37,42 @@ check() { fi } -check "$APP/symfony/src/Entity/Todo.php" "$SCRIPT_DIR/Todo.php" -check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoController.php" -check "$APP/qml/TodoList.qml" "$SCRIPT_DIR/TodoList.qml" +clear_outputs() { + rm -f "$APP/symfony/src/Entity/Todo.php" + rm -f "$APP/symfony/src/Controller/TodoController.php" + rm -f "$APP/symfony/src/Dto/CreateTodoDto.php" + rm -f "$APP/symfony/src/Dto/UpdateTodoDto.php" + rm -f "$APP/qml/TodoList.qml" +} + +# ── Mode 1: legacy (no --with-dto) ──────────────────────────────────── +clear_outputs +( cd "$APP/symfony" \ + && bin/console make:bridge:resource Todo --no-interaction >/dev/null \ + && bin/console make:bridge:command MarkAllDone --no-interaction >/dev/null \ + && bin/console make:bridge:window Todo --no-interaction >/dev/null ) + +check "$APP/symfony/src/Entity/Todo.php" "$SCRIPT_DIR/Todo.php" +check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoController.php" +check "$APP/qml/TodoList.qml" "$SCRIPT_DIR/TodoList.qml" check "$APP/symfony/src/Controller/MarkAllDoneController.php" "$SCRIPT_DIR/MarkAllDoneController.php" -check "$APP/qml/TodoWindow.qml" "$SCRIPT_DIR/TodoWindow.qml" +check "$APP/qml/TodoWindow.qml" "$SCRIPT_DIR/TodoWindow.qml" + +# ── Mode 2: --with-dto (re-runs make:bridge:resource only) ──────────── +# The entity + QML output is byte-identical between modes; only the +# controller swaps and the two DTOs appear. Re-checking the unchanged +# outputs would just be noise. +clear_outputs +( cd "$APP/symfony" \ + && bin/console make:bridge:resource Todo --with-dto --no-interaction >/dev/null ) + +check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoControllerWithDto.php" +check "$APP/symfony/src/Dto/CreateTodoDto.php" "$SCRIPT_DIR/CreateTodoDto.php" +check "$APP/symfony/src/Dto/UpdateTodoDto.php" "$SCRIPT_DIR/UpdateTodoDto.php" if [ "$fail" -ne 0 ]; then echo "Snapshot test failed. If the change is intended, update the baselines under $SCRIPT_DIR/." >&2 exit 1 fi -echo "All maker outputs match snapshots." +echo "All maker outputs match snapshots (legacy + --with-dto modes)." diff --git a/framework/skeleton/symfony/composer.json b/framework/skeleton/symfony/composer.json index 5a8b2d6..552bbb1 100644 --- a/framework/skeleton/symfony/composer.json +++ b/framework/skeleton/symfony/composer.json @@ -11,6 +11,7 @@ "symfony/security-bundle": "^8.0", "symfony/mercure-bundle": "^0.4", "symfony/uid": "^8.0", + "symfony/validator": "^8.0", "doctrine/orm": "^3.0", "doctrine/doctrine-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^4.0", diff --git a/framework/skeleton/symfony/composer.lock b/framework/skeleton/symfony/composer.lock index 09af9be..b98e8ed 100644 --- a/framework/skeleton/symfony/composer.lock +++ b/framework/skeleton/symfony/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": "dd53e6e42aa4773eaed84e9eaa374a68", + "content-hash": "c339068ebced1f2d3b2ce954e79f5ea6", "packages": [ { "name": "doctrine/collections", @@ -1199,13 +1199,13 @@ "dist": { "type": "path", "url": "../../php", - "reference": "68fca95525db2311a08deb931f1b92909b20c450" + "reference": "b426d4a8ca67cde4f3bd0471d340e348b1fd4053" }, "require": { "doctrine/dbal": "^4.0", "doctrine/doctrine-bundle": "^3.0", "doctrine/orm": "^3.0", - "php": "^8.3", + "php": "^8.4", "symfony/config": "^8.0", "symfony/console": "^8.0", "symfony/dependency-injection": "^8.0", @@ -1259,7 +1259,7 @@ ] }, "license": [ - "proprietary" + "LGPL-3.0-or-later" ], "description": "Symfony bundle bridging PHP applications to a Qt/QML host (part of the php-qml framework).", "transport-options": { @@ -5024,6 +5024,88 @@ ], "time": "2026-03-30T15:14:47+00:00" }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, { "name": "symfony/type-info", "version": "v8.0.9", @@ -5184,6 +5266,101 @@ ], "time": "2026-04-30T16:10:06+00:00" }, + { + "name": "symfony/validator", + "version": "v8.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "131dc8322c06595a6c98185787fa756deada20df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/131dc8322c06595a6c98185787fa756deada20df", + "reference": "131dc8322c06595a6c98185787fa756deada20df", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/doctrine-bridge": "<7.4", + "symfony/expression-language": "<7.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/type-info": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v8.0.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-30T16:10:06+00:00" + }, { "name": "symfony/var-dumper", "version": "v8.0.8", @@ -5743,7 +5920,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.6.0"