├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── dto.php └── src ├── Console ├── Commands │ └── MakeDtoCommand.php ├── DefaultDtoQualifier.php ├── DtoGenerationData.php ├── DtoQualifierContract.php ├── Manifest.php ├── ModelPropertiesMapper.php └── stubs │ └── dto.stub ├── Dto.php ├── Factories ├── DtoFactory.php ├── DtoFactoryContract.php └── ModelDtoFactory.php ├── Manipulators ├── CarbonConverter.php └── Listener.php ├── Providers └── LaravelDtoServiceProvider.php └── Traits └── TurnsIntoDto.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-dto` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | 8 | ## 2.2.1 ... 2.2.2 - 2021-05-17 9 | 10 | ### Fixed 11 | - GitHub action build 12 | 13 | 14 | ## 2.2.0 - 2021-05-17 15 | 16 | ### Changed 17 | - Update package to support Laravel Octane 18 | 19 | 20 | ## 2.1.1 - 2020-04-06 21 | 22 | ### Changed 23 | - Dependencies were updated 24 | 25 | 26 | ## 2.1.0 - 2020-12-13 27 | 28 | ### Added 29 | - Global DTO flags in configuration 30 | 31 | ### Changed 32 | - Minimum version of DTO package in use 33 | 34 | 35 | ## 2.0.0 - 2020-11-07 36 | 37 | ### Added 38 | - Support for DTO 2 39 | - Support for Laravel 8 40 | - Support for PHP 8 41 | - Improved DTO debugging 42 | 43 | ### Changed 44 | - Tag name for publishing the configuration 45 | 46 | ### Removed 47 | - Support for DTO 1 48 | - Support for PHP 7.1 and 7.2 49 | 50 | 51 | ## 1.0.0 - 2020-04-20 52 | 53 | ### Added 54 | - Artisan command to generate DTOs for Eloquent models 55 | - Factory methods to instantiate a DTO from common Laravel interfaces 56 | - DTO injection resolution via IoC container 57 | - Carbon values converter 58 | - Injected dependency resolution in listeners 59 | - Support for macros in DTOs 60 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `andrea.marco.sartori@gmail.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/cerbero90/laravel-dto). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andrea Marco Sartori 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel DTO 2 | 3 | [![Author][ico-author]][link-author] 4 | [![PHP Version][ico-php]][link-php] 5 | [![Laravel Version][ico-laravel]][link-laravel] 6 | [![Octane Compatibility][ico-octane]][link-octane] 7 | [![Build Status][ico-actions]][link-actions] 8 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 9 | [![Quality Score][ico-code-quality]][link-code-quality] 10 | [![Latest Version][ico-version]][link-packagist] 11 | [![Software License][ico-license]](LICENSE.md) 12 | [![PSR-12][ico-psr12]][link-psr12] 13 | [![Total Downloads][ico-downloads]][link-downloads] 14 | 15 | Laravel DTO integrates [DTO][link-dto], a package inspired by [Lachlan Krautz][link-lachlan]' excellent [data-transfer-object][link-data-transfer-object], with the functionalities of Laravel. 16 | 17 | A data transfer object (DTO) is an object that carries data between processes. DTO does not have any behaviour except for storage, retrieval, serialization and deserialization of its own data. DTOs are simple objects that should not contain any business logic but rather be used for transferring data. 18 | 19 | Below are explained the advantages brought by this package in a Laravel application. In order to discover all the features of DTO, please refer to the [full DTO documentation][link-dto]. 20 | 21 | 22 | ## Install 23 | 24 | Via Composer: 25 | 26 | ```bash 27 | composer require cerbero/laravel-dto 28 | ``` 29 | 30 | To customize some aspects of this package, the `config/dto.php` file can optionally be generated via: 31 | 32 | ```bash 33 | php artisan vendor:publish --tag=dto 34 | ``` 35 | 36 | 37 | ## Usage 38 | 39 | * [Generate DTOs](#generate-dtos) 40 | * [Instantiate a DTO](#instantiate-a-dto) 41 | * [Resolve a DTO](#resolve-a-dto) 42 | * [Convert into DTO](#convert-into-dto) 43 | * [Convert into array](#convert-into-array) 44 | * [Listen to events](#listen-to-events) 45 | * [Support for macros](#support-for-macros) 46 | * [DTO debugging](#dto-debugging) 47 | 48 | 49 | ### Generate DTOs 50 | 51 | DTOs for Eloquent models can be automatically generated by running the following Artisan command: 52 | 53 | ```bash 54 | php artisan make:dto App/User 55 | ``` 56 | 57 | The database table of the specified model is scanned to populate the DTO properties. Furthermore, if the model has relationships, a DTO is also generated for each related model. For example, if our `User` model looks like: 58 | 59 | ```php 60 | class User extends Model 61 | { 62 | public function posts() 63 | { 64 | return $this->hasMany('App\Post'); 65 | } 66 | } 67 | ``` 68 | 69 | The DTOs `App\Dtos\UserData` and `App\Dtos\PostData` are generated like so: 70 | 71 | ```php 72 | use Cerbero\LaravelDto\Dto; 73 | use Carbon\Carbon; 74 | 75 | use const Cerbero\Dto\PARTIAL; 76 | use const Cerbero\Dto\IGNORE_UNKNOWN_PROPERTIES; 77 | 78 | /** 79 | * The data transfer object for the User model. 80 | * 81 | * @property int $id 82 | * @property string $name 83 | * @property Carbon $createdAt 84 | * @property Carbon $updatedAt 85 | * @property PostData[] $posts 86 | */ 87 | class UserData extends Dto 88 | { 89 | /** 90 | * The default flags. 91 | * 92 | * @var int 93 | */ 94 | protected static $defaultFlags = PARTIAL | IGNORE_UNKNOWN_PROPERTIES; 95 | } 96 | 97 | /** 98 | * The data transfer object for the Post model. 99 | * 100 | * @property int $id 101 | * @property string $content 102 | * @property int $userId 103 | * @property Carbon $createdAt 104 | * @property Carbon $updatedAt 105 | * @property UserData $user 106 | */ 107 | class PostData extends Dto 108 | { 109 | /** 110 | * The default flags. 111 | * 112 | * @var int 113 | */ 114 | protected static $defaultFlags = PARTIAL | IGNORE_UNKNOWN_PROPERTIES; 115 | } 116 | ``` 117 | 118 | By default, DTOs are generated in the `Dtos` directory which is created where models are. For example the DTO for `App\User` is generated as `App\Dtos\UserData` and the DTO for `App\Users\User` is generated as `App\Users\Dtos\UserData`. 119 | 120 | To change either the location or the suffix `Data` of generated DTOs, we can create a DTO qualifier by implementing the interface `DtoQualifierContract` and replace the default qualifier in `config/dto.php`. The example below qualifies a DTO in the directory of the model and adds the suffix `Dto`: 121 | 122 | ```php 123 | use Cerbero\LaravelDto\DtoQualifierContract; 124 | 125 | class MyDtoQualifier implements DtoQualifierContract 126 | { 127 | public function qualify(string $model): string 128 | { 129 | return $model . 'Dto'; 130 | } 131 | } 132 | 133 | // in config/dto.php 134 | return [ 135 | 'qualifier' => MyDtoQualifier::class, 136 | ]; 137 | ``` 138 | 139 | Finally, if a model has already its own DTO generated, we can overwrite it with the option `--force` or `-f`: 140 | 141 | ```bash 142 | php artisan make:dto App/User --force 143 | ``` 144 | 145 | 146 | ### Instantiate a DTO 147 | 148 | In addition to the [traditional ways to instantiate a DTO][link-dto-init], Laravel DTO provides handy methods to create a new instance of DTO from HTTP requests, Eloquent models or other common interfaces present in Laravel. 149 | 150 | For example `UserData` can be instantiated from an HTTP request by calling the method `fromRequest()`: 151 | 152 | ```php 153 | use App\Dtos\UserData; 154 | use App\Http\Controllers\Controller; 155 | use Illuminate\Http\Request; 156 | 157 | class UserController extends Controller 158 | { 159 | public function store(Request $request) 160 | { 161 | $dto = UserData::fromRequest($request); 162 | } 163 | } 164 | ``` 165 | 166 | The request passed to the method `fromRequest()` is optional: if not provided, the current application request is used to instantiate `UserData`. 167 | 168 | By default the flags [PARTIAL][link-flag-partial] and [IGNORE_UNKNOWN_PROPERTIES][link-flag-ignore] are applied to the DTO when it is instantiated from a request. [Additional flags][link-flags] can be passed as second parameter to further customize the behaviour of the DTO. 169 | 170 | --- 171 | 172 | To instantiate a DTO from an Eloquent model, we can call the method `fromModel()`: 173 | 174 | ```php 175 | $user = new User(['name' => 'Phil']); 176 | 177 | $dto = UserData::fromModel($user); 178 | ``` 179 | 180 | The flags [PARTIAL][link-flag-partial], [IGNORE_UNKNOWN_PROPERTIES][link-flag-ignore] and [CAST_PRIMITIVES][link-flag-cast] are applied to the DTO when it is instantiated from a model. [Additional flags][link-flags] can be passed as second parameter. 181 | 182 | --- 183 | 184 | Finally, the method `from()` instantiates a DTO from several interfaces (specific to Laravel or not), including: 185 | 186 | - `Illuminate\Support\Enumerable` 187 | - `Illuminate\Contracts\Support\Arrayable` 188 | - `Illuminate\Contracts\Support\Jsonable` 189 | - `JsonSerializable` 190 | - `Traversable` 191 | - any value that can be casted into an array 192 | 193 | In this case no flags are applied by default, but they can still be passed as second parameter. 194 | 195 | 196 | ### Resolve a DTO 197 | 198 | As long as [PARTIAL][link-flag-partial] is set in the default flags of a DTO, such DTO can be automatically resolved by the Laravel IoC container with the data carried by the current application request: 199 | 200 | ```php 201 | use App\Dtos\UserData; 202 | use App\Http\Controllers\Controller; 203 | 204 | class UserController extends Controller 205 | { 206 | public function store(UserData $dto) 207 | { 208 | // ... 209 | } 210 | } 211 | ``` 212 | 213 | 214 | ### Convert into DTO 215 | 216 | Another way to get an instance of DTO from different objects is letting them use the trait `TurnsIntoDto` and call the method `toDto()`: 217 | 218 | ```php 219 | use Cerbero\LaravelDto\Traits\TurnsIntoDto; 220 | 221 | class StoreUserRequest extends Request 222 | { 223 | use TurnsIntoDto; 224 | } 225 | 226 | class User extends Model 227 | { 228 | use TurnsIntoDto; 229 | 230 | protected $dtoClass = UserData::class; 231 | } 232 | 233 | class Example 234 | { 235 | use TurnsIntoDto; 236 | 237 | protected function getDtoClass(): ?string 238 | { 239 | return $condition ? UserData::class : OtherDto::class; 240 | } 241 | } 242 | 243 | $dto = $request->toDto(UserData::class, MUTABLE); 244 | $dto = $user->toDto(CAST_PRIMITIVES); 245 | $dto = $example->toDto(); 246 | ``` 247 | 248 | Classes using the trait can specify the DTO to turn into by: 249 | - passing the DTO class name as first parameter of the method `toDto()` 250 | - defining the property `$dtoClass` 251 | - overriding the method `getDtoClass()` if custom logic is needed 252 | 253 | Flags can optionally be passed as second parameter, or first parameter if the DTO class is already defined in the class using the trait. When models turn into DTOs, the flag [CAST_PRIMITIVES][link-flag-cast] is added to help casting values if casts are not defined on the Eloquent models. 254 | 255 | 256 | ### Convert into array 257 | 258 | By default Laravel DTO registers a [value converter][link-value-converter] for `Carbon` instances. When a DTO is converted into array, all its `Carbon` objects are turned into an atom string and then converted back into `Carbon` instances when a new DTO is instantiated: 259 | 260 | ```php 261 | $dto = UserData::make(['created_at' => '2000-01-01']); 262 | $dto->createdAt; // Carbon instance 263 | $data = $dto->toArray(); // ['created_at' => '2000-01-01T00:00:00+00:00'] 264 | 265 | $dto = UserData::make($data); 266 | $dto->createdAt; // Carbon instance 267 | ``` 268 | 269 | Conversions can be added or removed in the `config/dto.php` file, specifically via the key `conversions`: 270 | 271 | ```php 272 | use Carbon\Carbon; 273 | use Cerbero\LaravelDto\Manipulators\CarbonConverter; 274 | 275 | return [ 276 | 'conversions' => [ 277 | Carbon::class => CarbonConverter::class, 278 | ], 279 | ]; 280 | ``` 281 | 282 | 283 | ### Listen to events 284 | 285 | The only feature added by this package to [listeners][link-listener] is the ability to resolve dependencies via the Laravel IoC container. Dependencies can be injected into listeners constructor to be automatically resolved. 286 | 287 | Listeners can be added or removed in the `config/dto.php` file, specifically via the key `listeners`: 288 | 289 | ```php 290 | return [ 291 | 'listeners' => [ 292 | UserData::class => UserDataListener::class, 293 | ], 294 | ]; 295 | ``` 296 | 297 | 298 | ### Define flags globally 299 | 300 | Sometimes we may want all our DTOs to share the same [flags][link-flags], an example might be the need to always work with mutable DTOs. An easy way to accomplish that is defining such flags in the `config/dto.php` file: 301 | 302 | ```php 303 | return [ 304 | 'flags' => MUTABLE, 305 | ]; 306 | ``` 307 | 308 | 309 | ### Support for macros 310 | 311 | In case we need to add functionalities to all DTOs, an option might be using macros. Please refer to the Laravel documentation to see an [example of how to register a macro][link-macros]. 312 | 313 | 314 | ### DTO debugging 315 | 316 | When using the helpers `dump()` or `dd()`, only DTOs data will be shown instead of all the underlying architecture that makes the package work: 317 | 318 | ```php 319 | dd($dto); 320 | 321 | // only DTO data is shown: 322 | 323 | App\Dtos\UserData {#3224 324 | +name: "Phil" 325 | } 326 | ``` 327 | 328 | 329 | ## Change log 330 | 331 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 332 | 333 | 334 | ## Testing 335 | 336 | ```bash 337 | composer test 338 | ``` 339 | 340 | 341 | ## Contributing 342 | 343 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. 344 | 345 | 346 | ## Security 347 | 348 | If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker. 349 | 350 | 351 | ## Credits 352 | 353 | - [Andrea Marco Sartori][link-author] 354 | - [All Contributors][link-contributors] 355 | 356 | 357 | ## License 358 | 359 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 360 | 361 | [ico-author]: https://img.shields.io/static/v1?label=author&message=cerbero90&color=50ABF1&logo=twitter&style=flat-square 362 | [ico-php]: https://img.shields.io/packagist/php-v/cerbero/laravel-dto?color=%234F5B93&logo=php&style=flat-square 363 | [ico-laravel]: https://img.shields.io/static/v1?label=laravel&message=%E2%89%A55.6&color=ff2d20&logo=laravel&style=flat-square 364 | [ico-octane]: https://img.shields.io/static/v1?label=octane&message=compatible&color=ff2d20&logo=laravel&style=flat-square 365 | [ico-version]: https://img.shields.io/packagist/v/cerbero/laravel-dto.svg?label=version&style=flat-square 366 | [ico-actions]: https://img.shields.io/github/workflow/status/cerbero90/laravel-dto/build?style=flat-square&logo=github 367 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 368 | [ico-psr12]: https://img.shields.io/static/v1?label=compliance&message=PSR-12&color=blue&style=flat-square 369 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cerbero90/laravel-dto.svg?style=flat-square&logo=scrutinizer 370 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/laravel-dto.svg?style=flat-square&logo=scrutinizer 371 | [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/laravel-dto.svg?style=flat-square 372 | 373 | [link-author]: https://twitter.com/cerbero90 374 | [link-php]: https://www.php.net 375 | [link-laravel]: https://laravel.com 376 | [link-octane]: https://github.com/laravel/octane 377 | [link-packagist]: https://packagist.org/packages/cerbero/laravel-dto 378 | [link-actions]: https://github.com/cerbero90/laravel-dto/actions?query=workflow%3Abuild 379 | [link-psr12]: https://www.php-fig.org/psr/psr-12/ 380 | [link-scrutinizer]: https://scrutinizer-ci.com/g/cerbero90/laravel-dto/code-structure 381 | [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/laravel-dto 382 | [link-downloads]: https://packagist.org/packages/cerbero/laravel-dto 383 | [link-dto]: https://github.com/cerbero90/dto 384 | [link-data-transfer-object]: https://github.com/rexlabsio/data-transfer-object 385 | [link-dto-init]: https://github.com/cerbero90/dto#instantiate-a-dto 386 | [link-flags]: https://github.com/cerbero90/dto#available-flags 387 | [link-flag-partial]: https://github.com/cerbero90/dto#partial 388 | [link-flag-ignore]: https://github.com/cerbero90/dto#ignore_unknown_properties 389 | [link-flag-cast]: https://github.com/cerbero90/dto#cast_primitives 390 | [link-value-converter]: https://github.com/cerbero90/dto#convert-into-array 391 | [link-listener]: https://github.com/cerbero90/dto#listen-to-events 392 | [link-macros]: https://laravel.com/docs/collections#extending-collections 393 | [link-author]: https://github.com/cerbero90 394 | [link-lachlan]: https://github.com/lachlankrautz 395 | [link-contributors]: ../../contributors 396 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerbero/laravel-dto", 3 | "type": "library", 4 | "description": "Data Transfer Object (DTO) for Laravel", 5 | "keywords": [ 6 | "laravel", 7 | "dto", 8 | "data transfer object" 9 | ], 10 | "homepage": "https://github.com/cerbero/laravel-dto", 11 | "license": "MIT", 12 | "authors": [{ 13 | "name": "Andrea Marco Sartori", 14 | "email": "andrea.marco.sartori@gmail.com", 15 | "homepage": "https://github.com/cerbero90", 16 | "role": "Developer" 17 | }], 18 | "require": { 19 | "php": "^7.3||^8.0", 20 | "cerbero/dto": "^2.1", 21 | "doctrine/dbal": "^3.0", 22 | "illuminate/console": ">=8.0", 23 | "illuminate/container": ">=8.0", 24 | "illuminate/database": ">=8.0", 25 | "illuminate/filesystem": ">=8.0", 26 | "illuminate/http": ">=8.0", 27 | "illuminate/support": ">=8.0" 28 | }, 29 | "require-dev": { 30 | "laravel/legacy-factories": "^1.1", 31 | "orchestra/testbench": ">=6.0", 32 | "phpunit/phpunit": ">=9.0", 33 | "squizlabs/php_codesniffer": "^3.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Cerbero\\LaravelDto\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Cerbero\\LaravelDto\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "phpunit", 47 | "check-style": "phpcs --standard=psr12 src", 48 | "fix-style": "phpcbf --standard=psr12 src" 49 | }, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-master": "1.0-dev" 53 | }, 54 | "laravel": { 55 | "providers": [ 56 | "Cerbero\\LaravelDto\\Providers\\LaravelDtoServiceProvider" 57 | ] 58 | } 59 | }, 60 | "config": { 61 | "sort-packages": true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/dto.php: -------------------------------------------------------------------------------- 1 | DefaultDtoQualifier::class, 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | DTO value conversions 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Sometimes we might want a specific value type to be converted when a DTO 28 | | turns into an array. Below you can set what class should be converted 29 | | and the converter that accommodates its conversion from/to the DTO. 30 | | 31 | */ 32 | 'conversions' => [ 33 | Carbon::class => CarbonConverter::class, 34 | ], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | DTO value listeners 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Whenever a DTO sets or gets one of its property values, a listener might 42 | | intercept such event and alter the outcome. This is handy for example 43 | | when a value needs to be processed before getting set or retrieved. 44 | | 45 | */ 46 | 'listeners' => [ 47 | // UserData::class => UserDataListener::class, 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | DTO global flags 53 | |-------------------------------------------------------------------------- 54 | | 55 | | The flags to apply to all DTOs by default. Multiple flags might be added 56 | | below by joining them with bitwise OR operators "|". These flags will 57 | | finally be merged with the default flags specified within each DTO. 58 | | 59 | */ 60 | 'flags' => NONE, 61 | ]; 62 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeDtoCommand.php: -------------------------------------------------------------------------------- 1 | qualifier = $qualifier; 69 | $this->manifest = $manifest; 70 | } 71 | 72 | /** 73 | * Execute the console command. 74 | * 75 | * @return bool|null 76 | * 77 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 78 | */ 79 | public function handle() 80 | { 81 | parent::handle(); 82 | 83 | $this->manifest->delete(); 84 | } 85 | 86 | /** 87 | * Get the stub file for the generator. 88 | * 89 | * @return string 90 | */ 91 | protected function getStub(): string 92 | { 93 | return __DIR__ . '/../stubs/dto.stub'; 94 | } 95 | 96 | /** 97 | * Parse the class name and format according to the root namespace. 98 | * 99 | * @param string $name 100 | * @return string 101 | */ 102 | protected function qualifyClass($name): string 103 | { 104 | return $this->type = $this->qualifier->qualify($name); 105 | } 106 | 107 | /** 108 | * Get the desired class name from the input. 109 | * 110 | * @return string 111 | */ 112 | protected function getNameInput(): string 113 | { 114 | $model = str_replace('/', '\\', parent::getNameInput()); 115 | 116 | if (is_subclass_of($model, Model::class)) { 117 | return $model; 118 | } 119 | 120 | throw new InvalidArgumentException("Invalid model [$model]"); 121 | } 122 | 123 | /** 124 | * Build the class with the given name. 125 | * 126 | * @param string $name 127 | * @return string 128 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 129 | */ 130 | protected function buildClass($name): string 131 | { 132 | $this->manifest->addStartingDto($name)->addGeneratingDto($name)->save(); 133 | 134 | $search = ['DummyModel', 'DummyProperties', 'DummyUseStatements']; 135 | $replace = [class_basename($this->getNameInput()), $this->getModelProperties(), $this->getUseStatements()]; 136 | $content = parent::buildClass($name); 137 | 138 | return $this->sortUseStatements(str_replace($search, $replace, $content)); 139 | } 140 | 141 | /** 142 | * Retrieve the given model properties 143 | * 144 | * @return string 145 | */ 146 | protected function getModelProperties(): string 147 | { 148 | $properties = ''; 149 | $map = $this->laravel->make(ModelPropertiesMapper::class)->map($this->getCommandDto()); 150 | $this->type = $this->manifest->getGeneratingDto(); 151 | 152 | foreach ($map as $name => $types) { 153 | foreach ($types as &$type) { 154 | $normalizedType = rtrim($type, '[]'); 155 | 156 | if ($this->shouldBeAddedToUseStatements($normalizedType)) { 157 | $this->useStatements[$normalizedType] = true; 158 | } 159 | 160 | $type = class_basename($type); 161 | } 162 | 163 | $type = implode('|', $types); 164 | $properties .= " * @property {$type} \${$name}\n"; 165 | } 166 | 167 | return rtrim($properties); 168 | } 169 | 170 | /** 171 | * Retrieve the DTO generation data 172 | * 173 | * @return DtoGenerationData 174 | */ 175 | protected function getCommandDto(): DtoGenerationData 176 | { 177 | return DtoGenerationData::make([ 178 | 'model_class' => $model = $this->getNameInput(), 179 | 'model' => new $model(), 180 | 'forced' => $this->option('force') ?: false, 181 | 'output' => $this->getOutput(), 182 | ]); 183 | } 184 | 185 | /** 186 | * Determine whether the given type should be added to use statements 187 | * 188 | * @param string $type 189 | * @return bool 190 | */ 191 | protected function shouldBeAddedToUseStatements(string $type): bool 192 | { 193 | if ($this->hasSameNamespace($type)) { 194 | return false; 195 | } 196 | 197 | $usesStartingDto = $this->manifest->isStartingDto($type) && !$this->manifest->isGenerating($type); 198 | $generatedOrGenerating = $this->manifest->generated($type) || $this->manifest->generating($type); 199 | 200 | return $usesStartingDto || $generatedOrGenerating || class_exists($type); 201 | } 202 | 203 | /** 204 | * Determine whether the given class has the same namespace of the DTO that is going to be generated 205 | * 206 | * @param string $class 207 | * @return bool 208 | */ 209 | protected function hasSameNamespace(string $class): bool 210 | { 211 | $segmentsClass = explode('\\', $class); 212 | $segmentsDto = explode('\\', $this->manifest->getGeneratingDto()); 213 | 214 | array_pop($segmentsClass); 215 | array_pop($segmentsDto); 216 | 217 | return $segmentsClass === $segmentsDto; 218 | } 219 | 220 | /** 221 | * Retrieve the use statements 222 | * 223 | * @return string|null 224 | */ 225 | protected function getUseStatements(): ?string 226 | { 227 | $useStatements = ''; 228 | 229 | foreach ($this->useStatements as $class => $value) { 230 | $useStatements .= "\nuse {$class};"; 231 | unset($this->useStatements[$class]); 232 | } 233 | 234 | return $useStatements; 235 | } 236 | 237 | /** 238 | * Previous versions of Laravel didn't sort imports. 239 | * Overriding this method and using sortUseStatements() instead keeps generated DTOs consistent across all versions 240 | * 241 | * @param string $stub 242 | * @return string 243 | */ 244 | protected function sortImports($stub) 245 | { 246 | return $stub; 247 | } 248 | 249 | /** 250 | * Alphabetically sorts the use statements for the given stub 251 | * 252 | * @param string $stub 253 | * @return string 254 | */ 255 | protected function sortUseStatements($stub) 256 | { 257 | preg_match('/(?P(?:use [^;]+;$\n?)+)/m', $stub, $match); 258 | 259 | $imports = explode("\n", trim($match['imports'])); 260 | 261 | sort($imports); 262 | 263 | return str_replace(trim($match['imports']), implode("\n", $imports), $stub); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Console/DefaultDtoQualifier.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 35 | } 36 | 37 | /** 38 | * Write content in the manifest 39 | * 40 | * @param string $key 41 | * @param mixed $value 42 | * @return self 43 | */ 44 | public function write(string $key, $value): self 45 | { 46 | $this->manifest = $this->read(); 47 | 48 | Arr::set($this->manifest, $key, $value); 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Retrieve the manifest content 55 | * 56 | * @param string|null $key 57 | * @param mixed $default 58 | * @return mixed 59 | */ 60 | public function read(string $key = null, $default = null) 61 | { 62 | if (!file_exists($this->filename)) { 63 | file_put_contents($this->filename, 'manifest)) { 67 | $this->manifest = require $this->filename; 68 | } 69 | 70 | return $key === null ? $this->manifest : Arr::get($this->manifest, $key, $default); 71 | } 72 | 73 | /** 74 | * Save the manifest 75 | * 76 | * @return self 77 | */ 78 | public function save(): self 79 | { 80 | file_put_contents($this->filename, 'read(), true) . ';'); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Delete the manifest 87 | * 88 | * @return void 89 | */ 90 | public function delete(): void 91 | { 92 | if (file_exists($this->filename)) { 93 | unlink($this->filename); 94 | } 95 | } 96 | 97 | /** 98 | * Add the given model DTO 99 | * 100 | * @param string $model 101 | * @param string $dto 102 | * @return self 103 | */ 104 | public function addDto(string $model, string $dto): self 105 | { 106 | $this->write("{$model}.dto", $dto); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Retrieve the given model DTO 113 | * 114 | * @param string $model 115 | * @return string|null 116 | */ 117 | public function getDto(string $model): ?string 118 | { 119 | return $this->read("{$model}.dto"); 120 | } 121 | 122 | /** 123 | * Add the given model use statements 124 | * 125 | * @param string $model 126 | * @param string $name 127 | * @param string $use 128 | * @return self 129 | */ 130 | public function addUseStatement(string $model, string $name, string $use): self 131 | { 132 | $this->write("{$model}.use.{$name}", $use); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Retrieve the given model use statements 139 | * 140 | * @param string $model 141 | * @return array 142 | */ 143 | public function getUseStatements(string $model): array 144 | { 145 | return $this->read("{$model}.use", []); 146 | } 147 | 148 | /** 149 | * Add the given DTO that is being generated 150 | * 151 | * @param string $dto 152 | * @return self 153 | */ 154 | public function addGeneratingDto(string $dto): self 155 | { 156 | $this->write("generating.{$dto}", true); 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Retrieve the DTO that is being generated now 163 | * 164 | * @return string|null 165 | */ 166 | public function getGeneratingDto(): ?string 167 | { 168 | if (empty($generating = $this->read('generating'))) { 169 | return null; 170 | } 171 | 172 | $dtos = array_keys($generating); 173 | 174 | return end($dtos); 175 | } 176 | 177 | /** 178 | * Determine whether the given DTO is being generated now 179 | * 180 | * @param string $dto 181 | * @return bool 182 | */ 183 | public function isGenerating(string $dto): bool 184 | { 185 | return $this->getGeneratingDto() === $dto; 186 | } 187 | 188 | /** 189 | * Determine whether the given DTO is in the process of being generated 190 | * 191 | * @param string $dto 192 | * @return bool 193 | */ 194 | public function generating(string $dto): bool 195 | { 196 | return $this->read("generating.{$dto}") !== null; 197 | } 198 | 199 | /** 200 | * Mark the latest generating DTO as generated 201 | * 202 | * @return self 203 | */ 204 | public function finishGeneratingDto(): self 205 | { 206 | if (!empty($this->read('generating'))) { 207 | $dto = $this->getGeneratingDto(); 208 | $this->write("generated.{$dto}", true); 209 | array_pop($this->manifest['generating']); 210 | } 211 | 212 | return $this; 213 | } 214 | 215 | /** 216 | * Determine whether the given DTO has been generated 217 | * 218 | * @param string $dto 219 | * @return bool 220 | */ 221 | public function generated(string $dto): bool 222 | { 223 | return $this->read("generated.{$dto}") !== null; 224 | } 225 | 226 | /** 227 | * Add the given class as starting DTO 228 | * 229 | * @param string $dto 230 | * @return self 231 | */ 232 | public function addStartingDto(string $dto): self 233 | { 234 | if ($this->read('starting') === null) { 235 | $this->write('starting', $dto); 236 | } 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Retrieve the starting DTO 243 | * 244 | * @return string|null 245 | */ 246 | public function getStartingDto(): ?string 247 | { 248 | return $this->read('starting'); 249 | } 250 | 251 | /** 252 | * Determine whether the given class is the starting DTO 253 | * 254 | * @param string $dto 255 | * @return bool 256 | */ 257 | public function isStartingDto(string $dto): bool 258 | { 259 | return $this->getStartingDto() === $dto; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Console/ModelPropertiesMapper.php: -------------------------------------------------------------------------------- 1 | 'string', 31 | 'boolean' => 'bool', 32 | 'datetime' => 'Carbon\Carbon', 33 | 'string' => 'string', 34 | 'json' => 'string', 35 | 'integer' => 'int', 36 | 'date' => 'Carbon\Carbon', 37 | 'smallint' => 'int', 38 | 'text' => 'string', 39 | 'decimal' => 'float', 40 | 'bigint' => 'int', 41 | ]; 42 | 43 | /** 44 | * The map determining whether a relation involves many models. 45 | * 46 | * @var array 47 | */ 48 | protected $relationsMap = [ 49 | 'hasOne' => false, 50 | 'morphOne' => false, 51 | 'belongsTo' => false, 52 | 'morphTo' => false, 53 | 'hasMany' => true, 54 | 'hasManyThrough' => true, 55 | 'morphMany' => true, 56 | 'belongsToMany' => true, 57 | 'morphToMany' => true, 58 | 'morphedByMany' => true, 59 | ]; 60 | 61 | /** 62 | * The manifest. 63 | * 64 | * @var Manifest 65 | */ 66 | protected $manifest; 67 | 68 | /** 69 | * The DTO qualifier. 70 | * 71 | * @var DtoQualifierContract 72 | */ 73 | protected $qualifier; 74 | 75 | /** 76 | * Instantiate the class. 77 | * 78 | * @param Manifest $manifest 79 | * @param DtoQualifierContract $qualifier 80 | */ 81 | public function __construct(Manifest $manifest, DtoQualifierContract $qualifier) 82 | { 83 | $this->manifest = $manifest; 84 | $this->qualifier = $qualifier; 85 | } 86 | 87 | /** 88 | * Retrieve the properties map of the given data to generate 89 | * 90 | * @param DtoGenerationData $data 91 | * @return array 92 | */ 93 | public function map(DtoGenerationData $data): array 94 | { 95 | $propertiesFromDatabase = $this->mapPropertiesFromDatabase($data); 96 | $propertiesFromRelations = $this->mapPropertiesFromRelations($data); 97 | 98 | return $propertiesFromDatabase + $propertiesFromRelations; 99 | } 100 | 101 | /** 102 | * Retrieve the given model properties from the database 103 | * 104 | * @param DtoGenerationData $data 105 | * @return array 106 | */ 107 | public function mapPropertiesFromDatabase(DtoGenerationData $data): array 108 | { 109 | $properties = []; 110 | $table = $data->model->getTable(); 111 | $connection = $data->model->getConnection(); 112 | 113 | foreach (Schema::getColumnListing($table) as $column) { 114 | $camelColumn = Str::camel($column); 115 | $rawType = Schema::getColumnType($table, $column); 116 | $types = [$this->schemaTypesMap[$rawType]]; 117 | 118 | if (!$connection->getDoctrineColumn($table, $column)->getNotnull()) { 119 | $types[] = 'null'; 120 | } 121 | 122 | $properties[$camelColumn] = $types; 123 | } 124 | 125 | return $properties; 126 | } 127 | 128 | /** 129 | * Retrieve the given model properties from its relations 130 | * 131 | * @param DtoGenerationData $data 132 | * @return array 133 | */ 134 | public function mapPropertiesFromRelations(DtoGenerationData $data): array 135 | { 136 | $properties = []; 137 | $relations = implode('|', array_keys($this->relationsMap)); 138 | $reflection = new ReflectionClass($data->model); 139 | $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); 140 | 141 | foreach ($methods as $method) { 142 | if ($method->getFileName() != $reflection->getFileName()) { 143 | continue; 144 | } 145 | 146 | if (!preg_match("/\\\$this->($relations)\W+([\w\\\]+)/", $this->getMethodBody($method), $matches)) { 147 | continue; 148 | } 149 | 150 | [, $relation, $relatedModel] = $matches; 151 | 152 | if (!$qualifiedModel = $this->qualifyModel($relatedModel, $reflection)) { 153 | continue; 154 | } 155 | 156 | $dto = $this->getDtoForModelOrGenerate($qualifiedModel, $data); 157 | $type = $this->relationsMap[$relation] ? $dto . '[]' : $dto; 158 | $properties += [$method->getName() => [$type]]; 159 | } 160 | 161 | return $properties; 162 | } 163 | 164 | /** 165 | * Retrieve the body of the given method 166 | * 167 | * @param ReflectionMethod $method 168 | * @return string 169 | */ 170 | protected function getMethodBody(ReflectionMethod $method): string 171 | { 172 | $file = $this->getFile($method->getFileName()); 173 | $offset = $method->getStartLine(); 174 | $length = $method->getEndLine() - $offset; 175 | 176 | return implode('', array_slice($file, $offset, $length)); 177 | } 178 | 179 | /** 180 | * Retrieve the given file as an array 181 | * 182 | * @param string $filename 183 | * @return array 184 | */ 185 | protected function getFile(string $filename): array 186 | { 187 | if ($this->cachedFile) { 188 | return $this->cachedFile; 189 | } 190 | 191 | return $this->cachedFile = file($filename); 192 | } 193 | 194 | /** 195 | * Retrieve the fully qualified class name of the given model 196 | * 197 | * @param string $model 198 | * @param ReflectionClass $reflection 199 | * @return string|null 200 | */ 201 | protected function qualifyModel(string $model, ReflectionClass $reflection): ?string 202 | { 203 | if (class_exists($model)) { 204 | return $model; 205 | } 206 | 207 | $useStatements = $this->getUseStatements($reflection); 208 | 209 | if (isset($useStatements[$model])) { 210 | return $useStatements[$model]; 211 | } 212 | 213 | return class_exists($class = $reflection->getNamespaceName() . "\\{$model}") ? $class : null; 214 | } 215 | 216 | /** 217 | * Retrieve the use statements of the given class 218 | * 219 | * @param ReflectionClass $reflection 220 | * @return array 221 | */ 222 | protected function getUseStatements(ReflectionClass $reflection): array 223 | { 224 | $class = $reflection->getName(); 225 | 226 | if ($useStatements = $this->manifest->getUseStatements($class)) { 227 | return $useStatements; 228 | } 229 | 230 | $file = $this->getFile($reflection->getFileName()); 231 | 232 | foreach ($file as $line) { 233 | if (strpos($line, 'class') === 0) { 234 | break; 235 | } elseif (strpos($line, 'use') === 0) { 236 | preg_match_all('/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i', $line, $matches, PREG_SET_ORDER); 237 | 238 | foreach ($matches as $match) { 239 | $segments = explode('\\', $match[1]); 240 | $name = $match[2] ?? end($segments); 241 | $this->manifest->addUseStatement($class, $name, $match[1]); 242 | } 243 | } 244 | } 245 | 246 | return $this->manifest->save()->getUseStatements($class); 247 | } 248 | 249 | /** 250 | * Retrieve the DTO class name for the given model 251 | * 252 | * @param string $model 253 | * @param DtoGenerationData $data 254 | * @return string 255 | */ 256 | protected function getDtoForModelOrGenerate(string $model, DtoGenerationData $data): string 257 | { 258 | if ($dto = $this->manifest->getDto($model)) { 259 | return $dto; 260 | } 261 | 262 | $dto = $this->qualifier->qualify($model); 263 | 264 | if ($this->shouldGenerateNestedDto($dto, $data->forced)) { 265 | Artisan::call('make:dto', [ 266 | 'name' => str_replace('\\', '/', $model), 267 | '--force' => $data->forced, 268 | ], $data->output); 269 | 270 | $this->manifest->finishGeneratingDto()->save(); 271 | } 272 | 273 | return $this->manifest->addDto($model, $dto)->save()->getDto($model); 274 | } 275 | 276 | /** 277 | * Determine whether the given nested DTO should be generated 278 | * 279 | * @param string $dto 280 | * @param bool $forced 281 | * @return bool 282 | */ 283 | protected function shouldGenerateNestedDto(string $dto, bool $forced): bool 284 | { 285 | if ($this->manifest->isStartingDto($dto) || $this->manifest->generating($dto)) { 286 | return false; 287 | } 288 | 289 | return $forced || !class_exists($dto); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Console/stubs/dto.stub: -------------------------------------------------------------------------------- 1 | all() : RequestFacade::all(); 42 | 43 | return static::make($data, $flags | PARTIAL | IGNORE_UNKNOWN_PROPERTIES); 44 | } 45 | 46 | /** 47 | * Retrieve an instance of DTO from the request 48 | * 49 | * @param Model $model 50 | * @param int $flags 51 | * @return self 52 | */ 53 | public static function fromModel(Model $model, int $flags = NONE): self 54 | { 55 | return static::make($model->toArray(), $flags | CAST_PRIMITIVES | PARTIAL | IGNORE_UNKNOWN_PROPERTIES); 56 | } 57 | 58 | /** 59 | * Retrieve an instance of DTO from the given source 60 | * 61 | * @param mixed $source 62 | * @param int $flags 63 | * @return self 64 | */ 65 | public static function from($source, int $flags = NONE): self 66 | { 67 | if ($source instanceof Enumerable) { 68 | $source = $source->all(); 69 | } elseif ($source instanceof Arrayable) { 70 | $source = $source->toArray(); 71 | } elseif ($source instanceof Jsonable) { 72 | $source = json_decode($source->toJson(), true); 73 | } elseif ($source instanceof JsonSerializable) { 74 | $source = $source->jsonSerialize(); 75 | } elseif ($source instanceof Traversable) { 76 | $source = iterator_to_array($source); 77 | } 78 | 79 | return static::make((array) $source, $flags); 80 | } 81 | 82 | /** 83 | * Retrieve the default flags 84 | * 85 | * @return int 86 | */ 87 | public static function getDefaultFlags(): int 88 | { 89 | return Config::get('dto.flags') | static::$defaultFlags; 90 | } 91 | 92 | /** 93 | * Retrieve the listener instance 94 | * 95 | * @return BaseListener 96 | */ 97 | protected function getListener(): BaseListener 98 | { 99 | return Listener::instance(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Factories/DtoFactory.php: -------------------------------------------------------------------------------- 1 | getNames(); 36 | 37 | foreach ($properties as $property) { 38 | $value = $this->getPropertyFromSource($property, $source); 39 | 40 | if ($value === static::MISSING_PROPERTY_TOKEN) { 41 | continue; 42 | } 43 | 44 | $data[$property] = $value; 45 | } 46 | 47 | return $dto::make($data, $flags); 48 | } 49 | 50 | /** 51 | * Retrieve the given property from the provided source 52 | * 53 | * @param string $property 54 | * @param mixed $source 55 | * @return mixed 56 | */ 57 | protected function getPropertyFromSource(string $property, $source) 58 | { 59 | $accessor = 'get' . Str::studly($property); 60 | 61 | if (method_exists($this, $accessor)) { 62 | return call_user_func([$this, $accessor], $source, $property); 63 | } 64 | 65 | return $this->getPropertyValueFromSource($property, $source); 66 | } 67 | 68 | /** 69 | * Retrieve the value for the given property from the provided source 70 | * 71 | * @param string $property 72 | * @param mixed $source 73 | * @return mixed 74 | */ 75 | protected function getPropertyValueFromSource(string $property, $source) 76 | { 77 | $snakeProperty = Str::snake($property); 78 | $camelProperty = Str::camel($property); 79 | 80 | return data_get($source, $snakeProperty, data_get($source, $camelProperty, static::MISSING_PROPERTY_TOKEN)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Factories/DtoFactoryContract.php: -------------------------------------------------------------------------------- 1 | getAttributes())) { 25 | return $attributes[$snakeProperty]; 26 | } 27 | 28 | if ($source->relationLoaded($snakeProperty)) { 29 | return $source->$snakeProperty->toArray(); 30 | } 31 | 32 | $camelProperty = Str::camel($property); 33 | 34 | if ($source->relationLoaded($camelProperty)) { 35 | return $source->$camelProperty->toArray(); 36 | } 37 | 38 | return static::MISSING_PROPERTY_TOKEN; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Manipulators/CarbonConverter.php: -------------------------------------------------------------------------------- 1 | toAtomString(); 23 | } 24 | 25 | /** 26 | * Convert the given value to be imported into a DTO. 27 | * 28 | * @param mixed $value 29 | * @return mixed 30 | */ 31 | public function toDto($value) 32 | { 33 | return Carbon::parse($value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Manipulators/Listener.php: -------------------------------------------------------------------------------- 1 | make($listener); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Providers/LaravelDtoServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 39 | $this->commands(MakeDtoCommand::class); 40 | } 41 | 42 | $this->publishes([static::CONFIG => $this->app->configPath('dto.php')], 'dto'); 43 | 44 | ArrayConverter::instance()->setConversions($this->config('conversions')); 45 | 46 | Listener::instance()->listen($this->config('listeners')); 47 | 48 | if (class_exists($cloner = '\Symfony\Component\VarDumper\Cloner\VarCloner')) { 49 | $cloner::$defaultCasters[Dto::class] = function (Dto $dto) { 50 | return $dto->toArray(); 51 | }; 52 | } 53 | } 54 | 55 | /** 56 | * Retrieve the given configuration value 57 | * 58 | * @param string $key 59 | * @return mixed 60 | */ 61 | protected function config(string $key) 62 | { 63 | return Config::get("dto.{$key}"); 64 | } 65 | 66 | /** 67 | * Register any application services. 68 | * 69 | * @return void 70 | */ 71 | public function register() 72 | { 73 | $this->mergeConfigFrom(static::CONFIG, 'dto'); 74 | 75 | $this->app->bind(DtoQualifierContract::class, $this->config('qualifier')); 76 | 77 | $this->app->singleton(Manifest::class, function () { 78 | $storagePath = Container::getInstance()->make('path.storage'); 79 | return new Manifest($storagePath . '/cerbero_laravel_dto.php'); 80 | }); 81 | 82 | $this->app->resolving(Dto::class, function (Dto $dto) { 83 | return $dto->mutate(function (Dto $dto) { 84 | $dto->merge(Request::all(), IGNORE_UNKNOWN_PROPERTIES); 85 | }); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Traits/TurnsIntoDto.php: -------------------------------------------------------------------------------- 1 | getDtoToTurnInto($dto); 36 | 37 | return $this->getDtoFactory()->make($dto, $this, $flags); 38 | } 39 | 40 | /** 41 | * Retrieve the DTO class to turn the current object into 42 | * 43 | * @param mixed $dto 44 | * @return string 45 | * @throws InvalidArgumentException 46 | * @throws DtoNotFoundException 47 | */ 48 | protected function getDtoToTurnInto($dto): string 49 | { 50 | $dto = is_string($dto) ? $dto : $this->getDtoClass(); 51 | 52 | if (!$dto) { 53 | throw new InvalidArgumentException('Unable to turn [' . static::class . '] into DTO, no class specified'); 54 | } elseif (!is_subclass_of($dto, Dto::class)) { 55 | throw new DtoNotFoundException($dto); 56 | } 57 | 58 | return $dto; 59 | } 60 | 61 | /** 62 | * Retrieve the DTO class 63 | * 64 | * @return string|null 65 | */ 66 | protected function getDtoClass(): ?string 67 | { 68 | return $this->dtoClass; 69 | } 70 | 71 | /** 72 | * Retrieve the DTO assembler 73 | * 74 | * @return DtoFactoryContract 75 | */ 76 | protected function getDtoFactory(): DtoFactoryContract 77 | { 78 | return $this instanceof Model ? new ModelDtoFactory() : new DtoFactory(); 79 | } 80 | } 81 | --------------------------------------------------------------------------------