{"id":20574736,"url":"https://github.com/macpaw/request-dto-resolver","last_synced_at":"2026-03-12T12:31:03.061Z","repository":{"id":231544261,"uuid":"756284764","full_name":"MacPaw/request-dto-resolver","owner":"MacPaw","description":"Symfony request resolver bundle","archived":false,"fork":false,"pushed_at":"2025-03-28T13:10:59.000Z","size":35,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":17,"default_branch":"develop","last_synced_at":"2025-05-08T08:46:30.612Z","etag":null,"topics":["backend","macpaw","symfony"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MacPaw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-02-12T11:08:48.000Z","updated_at":"2025-03-28T13:04:53.000Z","dependencies_parsed_at":"2024-04-04T15:01:14.133Z","dependency_job_id":"e8689524-d7e8-49bf-9f4a-cd9695a0434d","html_url":"https://github.com/MacPaw/request-dto-resolver","commit_stats":null,"previous_names":["macpaw/request-dto-resolver"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2Frequest-dto-resolver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2Frequest-dto-resolver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2Frequest-dto-resolver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2Frequest-dto-resolver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MacPaw","download_url":"https://codeload.github.com/MacPaw/request-dto-resolver/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253321892,"owners_count":21890488,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","macpaw","symfony"],"created_at":"2024-11-16T05:36:53.819Z","updated_at":"2025-12-16T10:23:51.839Z","avatar_url":"https://github.com/MacPaw.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Symfony Request DTO Resolver Bundle\n\nAutomatically resolves and validates Symfony HTTP request data (JSON, form-data, query parameters) into DTOs.\n\n## Features\n\n- Automatic resolution of request data into DTOs.\n- Seamless support for JSON, form-data, and query string parameters.\n- Built-in validation using the Symfony Form component.\n- Support for complex nested data structures.\n- Customizable parameter resolution order and field mapping.\n- Smart integration with other bundles that parse the request body.\n\n## Installation\n\n```console\ncomposer require macpaw/request-dto-resolver\n```\n\nThe bundle should be automatically registered in your `config/bundles.php`. If not, add it manually:\n\n```php\n// config/bundles.php\nreturn [\n    RequestDtoResolver\\RequestDtoResolverBundle::class =\u003e ['all' =\u003e true],\n    // ...\n];\n```\n\n## Configuration\n\nFirst, define an interface that your DTOs will implement. This allows the resolver to identify which arguments to process.\n\n```php\n// src/DTO/RequestDtoInterface.php\nnamespace App\\DTO;\n\ninterface RequestDtoInterface\n{\n}\n```\n\nThen, point the bundle to this interface in a configuration file:\n\n```yaml\n# config/packages/request_dto_resolver.yaml\nrequest_dto_resolver:\n    target_dto_interface: App\\DTO\\RequestDtoInterface\n```\n\n## How It Works\n\nThe resolver uses a combination of a DTO class and a Symfony Form to process and validate incoming request data.\n\n1.  **Controller Argument**: You type-hint a controller argument with your DTO class (e.g., `UserDto`).\n2.  **FormType Attribute**: You decorate the controller action with the `#[FormType]` attribute, specifying which Symfony Form to use for processing.\n3.  **Data Resolution**: The resolver extracts data from the request based on the form's fields.\n4.  **Validation**: The form validates the data against the constraints defined in your DTO.\n5.  **DTO Hydration**: If validation passes, a new DTO instance is created and populated with the validated data.\n\n## Usage\n\n### 1. Create a DTO\n\nThe DTO is a simple PHP class that implements your marker interface. Use Symfony's Validator components to define constraints.\n\n```php\n// src/DTO/UserDto.php\nnamespace App\\DTO;\n\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\nclass UserDto implements RequestDtoInterface\n{\n    #[Assert\\NotBlank]\n    #[Assert\\Length(min: 3)]\n    public string $name;\n\n    #[Assert\\NotBlank]\n    #[Assert\\Email]\n    public string $email;\n\n    /** @var string[] */\n    #[Assert\\Count(min: 1)]\n    #[Assert\\All([\n        new Assert\\NotBlank,\n        new Assert\\Length(min: 2)\n    ])]\n    public array $tags = [];\n}\n```\n\n### 2. Create a Form Type\n\nThe Form Type defines the structure of the expected request data and maps it to your DTO.\n\n```php\n// src/Form/UserFormType.php\nnamespace App\\Form;\n\nuse App\\DTO\\UserDto;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\n\nclass UserFormType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder\n            -\u003eadd('name', TextType::class)\n            -\u003eadd('email', EmailType::class)\n            -\u003eadd('tags', CollectionType::class, [\n                'entry_type' =\u003e TextType::class,\n                'allow_add' =\u003e true,\n            ]);\n    }\n\n    public function configureOptions(OptionsResolver $resolver): void\n    {\n        $resolver-\u003esetDefaults([\n            'data_class' =\u003e UserDto::class,\n        ]);\n    }\n}\n```\n\n### 3. Use in a Controller\n\nIn your controller, type-hint the action argument with your DTO class and add the `#[FormType]` attribute.\n\n```php\n// src/Controller/UserController.php\nnamespace App\\Controller;\n\nuse App\\DTO\\UserDto;\nuse App\\Form\\UserFormType;\nuse RequestDtoResolver\\Attribute\\FormType;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\n\nclass UserController\n{\n    #[FormType(UserFormType::class)]\n    public function __invoke(UserDto $dto): JsonResponse\n    {\n        // $dto is now a validated and populated object\n        return new JsonResponse([\n            'name' =\u003e $dto-\u003ename,\n            'email' =\u003e $dto-\u003eemail,\n            'tags' =\u003e $dto-\u003etags,\n        ]);\n    }\n}\n```\n\n## Parameter Resolution\n\nThe resolver automatically extracts data from the request to populate the form. The source of the data depends on the request's `Content-Type` header and method.\n\n### Resolution Order\n\nFor each field defined in your Form Type, the resolver searches for a corresponding value in the following order:\n\n1.  **JSON Body**: If the request has a `Content-Type` of `application/json`, the decoded JSON body is checked first.\n2.  **Query \u0026 Form Data**: The resolver then checks `request-\u003equery` (for `GET` parameters) and `request-\u003erequest` (for `POST` form data).\n3.  **Request Headers**: Finally, it checks the request headers.\n\nThis order means that for a `POST` request with both a JSON body and query parameters, the values in the **JSON body will take precedence**.\n\n### Common Scenarios\n\n-   **POST with JSON Body**: `{\"name\": \"John\"}` -\u003e `name` is resolved from JSON.\n-   **POST with Form Data**: `name=John` -\u003e `name` is resolved from form data.\n-   **GET with Query Parameters**: `?name=John` -\u003e `name` is resolved from query string.\n-   **GET with `Content-Type: application/json`**: The resolver will correctly ignore the header and still pull data from the query string, preventing malformed body errors.\n-   **Request without `Content-Type`**: The request is treated as a standard form/query request, and data is resolved from the query string.\n\n## Advanced Features\n\n### Custom Field Mapping\n\nYou can map request fields to different DTO properties using the `lookupKey` option in your Form Type. This is useful for handling request keys that don't match your DTO property names (e.g., `user-id` vs. `userId`).\n\n**Form Type Configuration:**\n\n```php\n// ...\n$builder-\u003eadd('userId', TextType::class, [\n    'attr' =\u003e ['lookupKey' =\u003e 'user-id'],\n]);\n// ...\n```\n\nThis configuration will map the `user-id` key from any source (JSON body, query, or header) to the `userId` form field.\n\n**Request Example:**\n\n```http\nPOST /api/some-endpoint\nContent-Type: application/json\n\n{\n    \"user-id\": 123\n}\n```\n\n## Integration with Other Bundles\n\nThis bundle is designed to work seamlessly with other bundles that parse the request body (e.g., FOSRestBundle). If the request body is already parsed and populated in `$request-\u003erequest`, the resolver will automatically use this pre-parsed data instead of reading the raw body again.\n\nThis ensures:\n-   No double-parsing of the request body.\n-   Consistent validation and mapping rules.\n-   Zero-configuration interoperability.\n\n## Error Handling\n\nThe bundle throws the following exceptions, which you can handle with a standard Symfony exception listener:\n\n-   `InvalidParamsDtoException`: For validation errors (contains a `ConstraintViolationList`).\n-   `BadRequestHttpException`: For a malformed JSON body.\n-   `UnsupportedMediaTypeHttpException`: For an unsupported `Content-Type`.\n-   `MissingFormTypeAttributeException`: When the `#[FormType]` attribute is missing on the controller action.\n\n## Contributing\n\nFeel free to open issues and submit pull requests.\n\n## License\n\nThis bundle is released under the MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmacpaw%2Frequest-dto-resolver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmacpaw%2Frequest-dto-resolver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmacpaw%2Frequest-dto-resolver/lists"}