{"id":19337041,"url":"https://github.com/tblindaruk/laravel-request-mapper","last_synced_at":"2025-04-23T01:30:55.151Z","repository":{"id":45313383,"uuid":"161887460","full_name":"TBlindaruk/laravel-request-mapper","owner":"TBlindaruk","description":"mapping laravel request to the DTO objects","archived":false,"fork":false,"pushed_at":"2021-12-22T01:37:04.000Z","size":90,"stargazers_count":33,"open_issues_count":1,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-02T06:12:17.274Z","etag":null,"topics":["dto","laravel","laravel-package","request-mapping"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TBlindaruk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-15T08:54:57.000Z","updated_at":"2023-07-15T13:00:33.000Z","dependencies_parsed_at":"2022-09-17T04:51:23.572Z","dependency_job_id":null,"html_url":"https://github.com/TBlindaruk/laravel-request-mapper","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TBlindaruk%2Flaravel-request-mapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TBlindaruk%2Flaravel-request-mapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TBlindaruk%2Flaravel-request-mapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TBlindaruk%2Flaravel-request-mapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TBlindaruk","download_url":"https://codeload.github.com/TBlindaruk/laravel-request-mapper/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250352193,"owners_count":21416454,"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":["dto","laravel","laravel-package","request-mapping"],"created_at":"2024-11-10T03:13:14.389Z","updated_at":"2025-04-23T01:30:54.868Z","avatar_url":"https://github.com/TBlindaruk.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Request Mapper for Laravel\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://travis-ci.org/TBlindaruk/laravel-request-mapper\"\u003e\u003cimg src=\"https://travis-ci.org/TBlindaruk/laravel-request-mapper.svg?branch=master\" alt=\"Build Status\"\u003e\u003c/a\u003e\n\u003ca href=\"https://codecov.io/gh/TBlindaruk/laravel-request-mapper/branch/master\"\u003e\u003cimg src=\"https://codecov.io/gh/TBlindaruk/laravel-request-mapper/branch/master/graph/badge.svg\" alt=\"Code Coverage\"\u003e\u003c/a\u003e\n\u003ca href=\"https://packagist.org/packages/maksi/laravel-request-mapper\"\u003e\u003cimg src=\"https://poser.pugx.org/maksi/laravel-request-mapper/v/stable.svg\" alt=\"Latest Stable Version\"\u003e\u003c/a\u003e\n\u003ca href=\"https://packagist.org/packages/maksi/laravel-request-mapper\"\u003e\u003cimg src=\"https://poser.pugx.org/maksi/laravel-request-mapper/license.svg\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nThis component allow you to inject DTO object mapped from the Request to the action.\n\n1. [Install](#install)\n2. [Requirements](#requirements)\n3. [Basic usage](#basic)\n4. [Nested object](#nested)\n5. [Mapped strategies](#mapped-strategies)\n6. [Create custom mapped strategy](#custom-mapped-strategy)\n7. [How to create an custom exception?](#change-exception)\n8. [Project example](#example)\n9. [Contributing](#contributing)\n10. [Licence](#licence)\n11. [TODO](#todo)\n\n\u003ca name=\"install\"\u003e \u003ch2\u003e1. Install \u003c/h2\u003e \u003c/a\u003e\n\nYou can install this package via composer using this command:\n\n```\ncomposer require maksi/laravel-request-mapper\n```\n\nThe package will automatically register itself.\n\n\u003ca name=\"requirements\"\u003e \u003ch2\u003e2. Requirements \u003c/h2\u003e \u003c/a\u003e\n\nPHP 7.1 or newer and Laravel 5.5 or newer\n\n\u003ca name=\"basic\"\u003e \u003ch2\u003e3. Basic usage \u003c/h2\u003e \u003c/a\u003e\n\n\u003cstrong\u003e3.1 Create an DTO object\u003c/strong\u003e\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\AllRequestData;\n\nfinal class RoomSearchRequestData extends AllRequestData\n{\n    private $name;\n \n    protected function init(array $data): void\n    {\n        $this-\u003ename = $data['name'] ?? null;\n    }\n\n    public function getName(): string\n    {\n        return $this-\u003ename;\n    }\n}\n```\n\nYour DTO object should extend one of RequestData classes:\n - [AllRequestData](./src/Filling/RequestData/AllRequestData.php)\n - [HeaderRequestData](./src/Filling/RequestData/HeaderRequestData.php)\n - [JsonRequestData](./src/Filling/RequestData/JsonRequestData.php)\n\nRequestData classes responsible for [mapped strategies](#mapped-strategies). \n\n`$data` array in the `init` it is an `array` which return from the [mapped strategies](#mapped-strategies) classes.  \u003cstrong\u003e Basically `$data` it is some data from the `Request`. \u003c/strong\u003e\n\n\u003cstrong\u003e3.2 Inject to the action\u003c/strong\u003e\n\nDTO object can be injected to any type of action.\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\n/**\n * @package App\\Http\\Controller\n */\nclass RoomSearchController\n{\n ...\n    public function __invoke(RoomSearchRequestData $payload) // DTO object injected\n    {\n        \n    }\n}\n\n```\n\n\u003cstrong\u003e3.3 Validate DTO object\u003c/strong\u003e\n\nYou can apply validation to the DTO object:\n- before mapping data to the DTO (`laravel` validation)\n- after mapping data to the DTO (`symfony annotation` validation)\n\n\u003cstrong\u003e3.3.1 Apply laravel validation \u003c/strong\u003e\n\n\u003ci\u003e Laravel validation applied for the `RequestData` object before object filling. \u003c/i\u003e \n\n1. You should create a class with validation rules. This class should implement `Maksi\\LaravelRequestMapper\\Validation\\BeforeType\\Laravel\\ValidationRuleInterface` interface (in case, if you do no need change the validation `messages` and `customAttributes`, than you can extend `Maksi\\LaravelRequestMapper\\Validation\\BeforeType\\Laravel\\AbstractValidationRule` class)\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace Maksi\\LaravelRequestMapper\\Tests\\Integration\\LaravelNestedValidation\\Stub;\n\nuse Maksi\\LaravelRequestMapper\\Validation\\BeforeType\\Laravel\\AbstractValidationRule;\n\nclass ValidatorRule extends AbstractValidationRule\n{\n    public function rules(): array\n    {\n        return [\n            'nested' =\u003e 'array|required',\n            'title' =\u003e 'string|required',\n        ];\n    }\n}\n\n```\n\n2. In the next you should apply this rules to the DTO object. This should be done via `annotation`.\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace Maksi\\LaravelRequestMapper\\Tests\\Integration\\LaravelNestedValidation\\Stub;\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\JsonRequestData;\nuse Maksi\\LaravelRequestMapper\\Validation\\BeforeType\\Laravel\\Annotation\\ValidationClass;\n\n/**\n * @ValidationClass(class=\"\\Maksi\\LaravelRequestMapper\\Tests\\Integration\\LaravelNestedValidation\\Stub\\ValidatorRule\")\n */\nclass RootRequestDataStub extends JsonRequestData\n{\n    private $title;\n\n    private $nested;\n\n    protected function init(array $data): void\n    {\n        $this-\u003etitle = $data['title'] ?? null;\n        $this-\u003enested = new NestedRequestDataStub($data['nested'] ?? []);\n    }\n\n    public function getTitle(): string\n    {\n        return $this-\u003etitle;\n    }\n\n    public function getNested(): NestedRequestDataStub\n    {\n        return $this-\u003enested;\n    }\n}\n\n```\n\nstring\n\n```PHP\n@ValidationClass(class=\"\\Maksi\\LaravelRequestMapper\\Tests\\Integration\\LaravelNestedValidation\\Stub\\ValidatorRule\")\n```\n\nindicates that \u003cstring\u003ebefore\u003c/string\u003e filling current DTO should be appied `\\Maksi\\LaravelRequestMapper\\Tests\\Integration\\LaravelNestedValidation\\Stub\\ValidatorRule` rules for the `data` which will be injected to the dto.\n\n\u003cstrong\u003e3.3.2 Apply symfony annotation validation \u003c/strong\u003e\n\nAnnotation symfony validation applied to the properties in the `RequestData` object (So this validation appied after the creating and DTO object).\n\nAt the first you should add the `@Type(type=\"annotation\")` annotation to the RequestData object. After this you can apply the validation to the DTO object (for more information please see symfony [validation documentation](https://symfony.com/doc/current/validation.html))\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace Maksi\\LaravelRequestMapper\\Tests\\Integration\\AnnotationValidation\\Stub;\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\AllRequestData;\nuse Maksi\\LaravelRequestMapper\\Validation\\Annotation\\Type;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n/**\n * @Type(type=\"annotation\")\n */\nclass AllRequestDataStub extends AllRequestData\n{\n    /**\n     * @Assert\\Type(type=\"int\")\n     * @Assert\\NotBlank()\n     */\n    private $allAge;\n\n    /**\n     * @var string\n     * @Assert\\NotBlank()\n     */\n    private $allTitle;\n\n    protected function init(array $data): void\n    {\n        $this-\u003eallAge = $data['age'] ?? null;\n        $this-\u003eallTitle = $data['title'] ?? null;\n    }\n\n    public function getAllTitle(): string\n    {\n        return $this-\u003eallTitle;\n    }\n\n    public function getAllAge(): int\n    {\n        return $this-\u003eallAge;\n    }\n}\n\n```\n\n\u003ca name=\"nested\"\u003e \u003ch2\u003e4. Nested object validation \u003c/h2\u003e \u003c/a\u003e\n\n\u003cstrong\u003e 4.1. Symfony annotation validation\u003c/strong\u003e\n\nIn the same way you can create an nested DTO object, \u003cstrong\u003e for example: \u003c/strong\u003e\n\n\u003cp align=\"center\"\u003e\u003ci\u003eRoot class\u003c/i\u003e\u003c/p\u003e\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace Maksi\\LaravelRequestMapper\\Tests\\Integration\\AnnotationNestedValidation\\Stub;\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\JsonRequestData;\nuse Maksi\\LaravelRequestMapper\\Validation\\Annotation\\Type;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n/**\n * @Type(type=\"annotation\")\n */\nclass RootRequestDataStub extends JsonRequestData\n{\n    /**\n     * @Assert\\NotBlank()\n     * @Assert\\Type(type=\"string\")\n     */\n    private $title;\n\n    /**\n     * @Assert\\Valid()\n     */\n    private $nested; // this property should have `Valid` annotation for validate nested object\n\n    protected function init(array $data): void\n    {\n        $this-\u003etitle = $data['title'] ?? null;\n        $this-\u003enested = new NestedRequestDataStub($data['nested'] ?? []);\n    }\n\n    public function getTitle(): string\n    {\n        return $this-\u003etitle;\n    }\n\n    public function getNested(): NestedRequestDataStub\n    {\n        return $this-\u003enested;\n    }\n}\n```\n\n\u003cp align=\"center\"\u003e\u003ci\u003eNested class\u003c/i\u003e\u003c/p\u003e\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace Maksi\\LaravelRequestMapper\\Tests\\Integration\\AnnotationNestedValidation\\Stub;\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\JsonRequestData;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\nclass NestedRequestDataStub extends JsonRequestData\n{\n    /**\n     * @Assert\\NotBlank()\n     * @Assert\\Type(type=\"string\")\n     */\n    private $nestedTitle;\n\n    protected function init(array $data): void\n    {\n        $this-\u003enestedTitle = $data['title'] ?? null;\n    }\n\n    public function getTitle(): string\n    {\n        return $this-\u003enestedTitle;\n    }\n}\n\n```\n\n\u003cstrong\u003e 4.2. Laravel validation for nested\u003c/strong\u003e\n\nSo, as a laravel validation applied before filling the `RequestData` object, than you should just create the same validation class as an for no nested validation.\n\n```PHP\n\u003c?php\nuse Maksi\\LaravelRequestMapper\\Validation\\BeforeType\\Laravel\\AbstractValidationRule;\n\nclass ValidatorRule extends AbstractValidationRule\n{\n    /**\n     * @return array\n     */\n    public function rules(): array\n    {\n        return [\n            'nested' =\u003e 'array|required',\n            'title' =\u003e 'string|required',\n            'nested.title' =\u003e 'string|required', // nested object validation\n        ];\n    }\n}\n```\n\n\u003ca name=\"mapped-strategies\"\u003e \u003ch2\u003e5.  Mapped strategies \u003c/h2\u003e \u003c/a\u003e\n\nBy default package has 3 strategies:\n- [AllStrategy](./src/Filling/Strategies/AllStrategy.php)\n- [HeaderStrategy](./src/Filling/Strategies/HeaderStrategy.php)\n- [JsonStrategy](./src/Filling/Strategies/JsonStrategy.php)\n\n\u003cstrong\u003e AllStrategy \u003c/strong\u003e - responsible for filling data from the `$request-\u003eall()` array. If you want to use this strategy, than your `RequestData` object should extend `AllRequestData` class.\n\n\u003cstrong\u003e HeaderStrategy \u003c/strong\u003e - responsible for filling data from the `$request-\u003eheader-\u003eall()` array. If you want to use this strategy, than your `RequestData` object should extend `HeaderRequestData` class.\n\n\u003cstrong\u003e JsonStrategy \u003c/strong\u003e - responsible for filling data from the `$request-\u003ejson()-\u003eall()` array. If you want to use this strategy, than your `RequestData` object should extend `JsonRequestData` class.\n\n\u003ca name=\"custom-mapped-strategy\"\u003e \u003ch2\u003e6.  Create custom mapped strategy \u003c/h2\u003e \u003c/a\u003e\n\nYou can create a custom mapped strategies for our application.\n\n\u003cstrong\u003e6.1 Create custom strategy \u003c/strong\u003e\n\nYou strategy should implement [StrategyInterface](./src/Filling/Strategies/StrategyInterface.php);\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace App\\Http\\RequestDataStrategy;\n\nuse App\\Http\\RequestData\\TeacherSearchRequestData;\nuse Illuminate\\Http\\Request;\nuse Maksi\\LaravelRequestMapper\\Filling\\Strategies\\StrategyInterface;\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\RequestData;\n\nclass TeacherSearchStrategy implements StrategyInterface\n{\n    public function resolve(Request $request): array\n    {\n        return $request-\u003eall();\n    }\n\n    public function support(Request $request, RequestData $object): bool\n    {\n        return $object instanceof TeacherSearchRequestData\n            \u0026\u0026 $request-\u003erouteIs('teacher-search');\n\n    }\n}\n```\n\nMethod `support` define is strategy available for `resolve` object. This method has 2 parameters `$request` and `$object`:\n- `$request` as a `Request` instance\n- \u003cstrong\u003e`$object`\u003c/strong\u003e - it is empty DTO instance, witch will be filled\n\nMethod `resolve` will return the array which will be injected to the DTO instance. This method accept `$request` object.\n\n\u003cstrong\u003e6.2 Create RequestData class for Strategy\u003c/strong\u003e\n\nYou should extend  \u003cstrong\u003e[RequestData](./src/Filling/RequestData/RequestData.php) in case if you want to create your own strategy\u003c/strong\u003e\n\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace App\\Http\\RequestData;\n\nuse Maksi\\LaravelRequestMapper\\Filling\\RequestData\\RequestData;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\nfinal class TeacherSearchRequestData extends RequestData\n{\n    /**\n     * @var string\n     *\n     * @Assert\\NotBlank()\n     * @Assert\\Type(type=\"string\")\n     */\n    private $name;\n\n    protected function init(array $data): void\n    {\n        $this-\u003ename = $data['name'] ?? null;\n    }\n\n    public function getName(): string\n    {\n        return $this-\u003ename;\n    }\n}\n```\n\n\u003cstrong\u003e6.3 Register your strategy in the `ServiceProvider` \u003c/strong\u003e\n\nYou should add instance of your `strategy` to the `Maksi\\LaravelRequestMapper\\StrategiesHandler` via `addStrategy` method.\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nnamespace App\\Http\\Provider;\n\nuse App\\Http\\RequestDataStrategy\\TeacherSearchStrategy;\nuse Illuminate\\Support\\ServiceProvider;\nuse Maksi\\LaravelRequestMapper\\FillingChainProcessor;\n\n/**\n * Class RequestMapperProvider\n *\n * @package App\\Http\\Provider\n */\nclass RequestMapperProvider extends ServiceProvider\n{\n    /**\n     * @param FillingChainProcessor $fillingChainProcessor\n     */\n    public function boot(FillingChainProcessor $fillingChainProcessor): void\n    {\n        $fillingChainProcessor-\u003eaddStrategy($this-\u003eapp-\u003emake(TeacherSearchStrategy::class));\n    }\n}\n\n```\n\n\n\u003ca name=\"change-exception\"\u003e \u003ch2\u003e7. Change validation exception \u003c/h2\u003e \u003c/a\u003e\n\n1. Create Exception which will extend `\\Maksi\\LaravelRequestMapper\\Validation\\ResponseException\\AbstractException` and implement toResponse method\n\nFor example:\n\n```PHP\n\u003c?php\n\nclass StringException extends \\Maksi\\LaravelRequestMapper\\Validation\\ResponseException\\AbstractException\n                                implements \\Illuminate\\Contracts\\Support\\Responsable\n{\n    /**\n     * Create an HTTP response that represents the object.\n     *\n     * @param  \\Illuminate\\Http\\Request $request\n     *\n     * @return \\Illuminate\\Http\\JsonResponse\n     */\n    public function toResponse($request)\n    {\n        return \\Illuminate\\Http\\JsonResponse::create('Invalid data provided')\n                            -\u003esetStatusCode(\\Illuminate\\Http\\Response::HTTP_UNPROCESSABLE_ENTITY);\n    }\n}\n```\n\n2. Define in `config/laravel-request-mapper.php` `exception-class` key\n\n```PHP\n\u003c?php\ndeclare(strict_types = 1);\n\nreturn [\n    'exception-class' =\u003e \\Maksi\\LaravelRequestMapper\\Validation\\ResponseException\\DefaultException::class,\n];\n\n```\n\n\u003ca name=\"example\"\u003e \u003ch2\u003e8. Project example \u003c/h2\u003e \u003c/a\u003e\n\nYou can see example of usage part of this package in https://github.com/E-ZSTU/rozklad-rest-api project. \n\n\u003ca name=\"contributing\"\u003e \u003ch2\u003e Contributing \u003c/h2\u003e \u003c/a\u003e\n\nPlease see [CONTRIBUTING](./CONTRIBUTING.md) for details.\n\n\u003ca name=\"license\"\u003e \u003ch2\u003e License \u003c/h2\u003e \u003c/a\u003e\n\nThe MIT License (MIT). Please see [License](./LICENSE) File for more information.\n\n\u003ca name=\"todo\"\u003e \u003ch2\u003eTODO \u003c/h2\u003e \u003c/a\u003e\n\n- [ ] think about https://www.reddit.com/r/laravel/comments/af843q/laravel_request_mapper/edx39cj\n- [ ] think about https://www.reddit.com/r/laravel/comments/af843q/laravel_request_mapper/edx8mci\n- [ ] delete symfony validation, since I`m not sure that is needed for the laravel community\n- [ ] add integration tests for `change exception`\n- [ ] add priority to the strategies\n- [ ] how you can get this DTO from the middleware (just register `RequestData` as a singleton)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftblindaruk%2Flaravel-request-mapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftblindaruk%2Flaravel-request-mapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftblindaruk%2Flaravel-request-mapper/lists"}