├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── src ├── Contracts │ └── DataTransferObject.php ├── DataTransferObject.php └── Providers │ └── Bootstrap.php └── tests ├── _bootstrap.php ├── _data └── dump.sql ├── _output └── .gitignore ├── _support ├── Helper │ └── Unit.php └── UnitTester.php ├── testCases └── helpers │ ├── Address.php │ ├── BadUnpopulatableObject.php │ ├── City.php │ ├── Contracts │ └── NotesInterface.php │ ├── Notes.php │ └── Person.php ├── unit.suite.yml └── unit ├── BootstrapTest.php ├── DataTransferObjectTest.php ├── NestedDataTransferObjectTest.php └── _bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .DS_Store 3 | .idea/ 4 | Thumbs.db 5 | composer.lock 6 | composer.phar 7 | tests/_output/* 8 | tests/_support/_generated/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.1' 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - $HOME/.composer/cache 11 | 12 | before_install: 13 | - travis_retry composer self-update 14 | 15 | install: 16 | - composer install --no-interaction --prefer-dist --no-suggest 17 | - composer update --no-interaction --prefer-stable --no-suggest 18 | 19 | before_script: 20 | 21 | script: 22 | - vendor/bin/codecept run -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 Alin Eugen Deac . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/aedart/data-transfer-object-dto.svg?branch=master)](https://travis-ci.org/aedart/data-transfer-object-dto) 2 | [![Latest Stable Version](https://poser.pugx.org/aedart/dto/v/stable)](https://packagist.org/packages/aedart/dto) 3 | [![Total Downloads](https://poser.pugx.org/aedart/dto/downloads)](https://packagist.org/packages/aedart/dto) 4 | [![Latest Unstable Version](https://poser.pugx.org/aedart/dto/v/unstable)](https://packagist.org/packages/aedart/dto) 5 | [![License](https://poser.pugx.org/aedart/dto/license)](https://packagist.org/packages/aedart/dto) 6 | 7 | # Deprecated - Data Transfer Object (DTO) 8 | 9 | Package has been replaced by [aedart/athenaeum](https://github.com/aedart/athenaeum) 10 | 11 | A variation / interpretation of the Data Transfer Object (DTO) design pattern (Distribution Pattern). A DTO is nothing more than an object that 12 | can hold some data. Most commonly it is used for for transporting that data between systems, e.g. a client and a server. 13 | 14 | This package provides an abstraction for such DTOs. 15 | 16 | If you don't know about DTOs, I recommend you to read [Martin Fowler's description](http://martinfowler.com/eaaCatalog/dataTransferObject.html) of DTO, and perhaps 17 | perform a few [Google searches](https://www.google.com/search?q=data+transfer+object&ie=utf-8&oe=utf-8) about this topic. 18 | 19 | ## Contents 20 | 21 | * [When to use this](#when-to-use-this) 22 | * [How to install](#how-to-install) 23 | * [Quick start](#quick-start) 24 | * [Advanced usage](#advanced-usage) 25 | * [Contribution](#contribution) 26 | * [Acknowledgement](#acknowledgement) 27 | * [Versioning](#versioning) 28 | * [License](#license) 29 | 30 | ## When to use this 31 | 32 | * When there is a strong need to interface DTOs, e.g. what properties must be available via getters and setters 33 | * When you need to encapsulate data that needs to be communicated between systems and or component instances 34 | 35 | Nevertheless, using DTOs can / will increase complexity of your project. Therefore, you should only use it, when you are really sure that you need them. 36 | 37 | ## How to install 38 | 39 | ```console 40 | composer require aedart/dto 41 | ``` 42 | 43 | This package uses [composer](https://getcomposer.org/). If you do not know what that is or how it works, I recommend that you read a little about, before attempting to use this package. 44 | 45 | ## Quick start 46 | 47 | ### Custom Interface for your DTO 48 | 49 | Start off by creating an interface for your DTO. Below is an example for a simple Person interface 50 | 51 | ```php 52 | name = $name; 112 | } 113 | 114 | /** 115 | * Get the person's name 116 | * 117 | * @return string 118 | */ 119 | public function getName() : ?string 120 | { 121 | return $this->name; 122 | } 123 | 124 | /** 125 | * Set the person's age 126 | * 127 | * @param int $age 128 | */ 129 | public function setAge(?int $age) 130 | { 131 | $this->age = $age; 132 | } 133 | 134 | /** 135 | * Get the person's age 136 | * 137 | * @return int 138 | */ 139 | public function getAge() : ?int 140 | { 141 | return $this->age; 142 | } 143 | 144 | } 145 | ``` 146 | 147 | Now you are ready to use the DTO. The following sections will highlight some of the usage scenarios. 148 | 149 | ### Property overloading 150 | 151 | Each defined property is accessible in multiple ways, if a getter and or setter method has been defined for that given property. 152 | 153 | For additional information, please read about [Mutators and Accessor](https://en.wikipedia.org/wiki/Mutator_method), [PHP's overloading](http://php.net/manual/en/language.oop5.overloading.php), 154 | and [PHP's Array-Access](http://php.net/manual/en/class.arrayaccess.php) 155 | 156 | ```php 157 | setName('John'); 164 | 165 | // But you can also just set the property itself 166 | $person->name = 'Jack'; // Will automatically invoke setName() 167 | 168 | // And you can also set it, using an array-accessor 169 | $person['name'] = 'Jane'; // Will also automatically invoke setName() 170 | 171 | // ... // 172 | 173 | // Obtain age using the regular getter method 174 | $age = $person->getAge(); 175 | 176 | // Can also get it via invoking the property directly 177 | $age = $person->age; // Will automatically invoke getAge() 178 | 179 | // Lastly, it can also be access via an array-accessor 180 | $age = $person['age']; // Also invokes the getAge() 181 | ``` 182 | 183 | #### Tip: PHPDoc's property-tag 184 | 185 | If you are using a modern [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment), then it will most likely support [PHPDoc](http://www.phpdoc.org/). 186 | 187 | By adding a [`@property`](http://www.phpdoc.org/docs/latest/references/phpdoc/tags/property.html) tag to your interface or concrete implementation, your IDE will be able to auto-complete the overloadable properties. 188 | 189 | ### Populating via an array 190 | 191 | You can populate your DTO using an array. 192 | 193 | ```php 194 | value array 197 | $data = [ 198 | 'name' => 'Timmy Jones', 199 | 'age' => 32 200 | ]; 201 | 202 | // Create instance and invoke populate 203 | $person = new Person(); 204 | $person->populate($data); // setName() and setAge() are invoked with the given values 205 | ``` 206 | 207 | If you are extending the default DTO abstraction, then you can also pass in an array in the constructor 208 | 209 | ```php 210 | value array 213 | $data = [ 214 | 'name' => 'Carmen Rock', 215 | 'age' => 25 216 | ]; 217 | 218 | // Create instance and invoke populate 219 | $person = new Person($data); // invokes populate(...), which then invokes the setter methods 220 | ``` 221 | 222 | ### Export properties to array 223 | 224 | Each DTO can be exported to an array. 225 | 226 | ```php 227 | toArray(); 231 | 232 | var_dump($properties); // Will output a "property-name => value" list 233 | // Example: 234 | // [ 235 | // 'name' => 'Timmy' 236 | // 'age' => 16 237 | // ] 238 | ``` 239 | 240 | ### Serialize to Json 241 | 242 | All DTOs are Json serializable, meaning that they inherit from the [`JsonSerializable`](http://php.net/manual/en/class.jsonserializable.php) interface. 243 | This means that when using `json_encode()`, the DTO automatically ensures that its properties are serializable by the encoding method. 244 | 245 | ```php 246 | 'Rian Dou', 250 | 'age' => 29 251 | ]); 252 | 253 | echo json_encode($person); 254 | ``` 255 | 256 | The above example will output the following; 257 | 258 | ``` json 259 | { 260 | "name":"Rian Dou", 261 | "age":29 262 | } 263 | ``` 264 | 265 | You can also perform json serialization directly on the DTO, by invoking the `toJson()` method. 266 | 267 | ```php 268 | 'Rian Dou', 272 | 'age' => 29 273 | ]); 274 | 275 | echo $person->toJson(); // The same as invoking json_encode($person); 276 | ``` 277 | 278 | ## Advanced usage 279 | 280 | ### Inversion of Control (IoC) / Dependency Injection 281 | 282 | In this interpretation of the DTO design pattern, each instance must hold a reference to an [IoC service container](http://laravel.com/docs/5.1/container). 283 | 284 | If you do not know what this means or how this works, please start off by reading the [wiki-article](https://en.wikipedia.org/wiki/Inversion_of_control) about it. 285 | 286 | #### Bootstrapping a service container #### 287 | 288 | If you are using this package inside a [Laravel](http://laravel.com/) application, then you can skip this part; **it is NOT needed!** 289 | 290 | ```php 291 | street = $street; 332 | } 333 | 334 | /** 335 | * Get the street 336 | * 337 | * @return string 338 | */ 339 | public function getStreet() : ?string 340 | { 341 | return $this->street; 342 | } 343 | } 344 | 345 | // You Person DTO now accepts an address object 346 | class Person extends DataTransferObject implements PersonInterface 347 | { 348 | 349 | protected $name = ''; 350 | 351 | protected $age = 0; 352 | 353 | protected $address = null; 354 | 355 | // ... getters and setters for name and age not shown ... // 356 | 357 | /** 358 | * Set the address 359 | * 360 | * @param Address $address 361 | */ 362 | public function setAddress(?Address $address) 363 | { 364 | $this->address = $address; 365 | } 366 | 367 | /** 368 | * Get the address 369 | * 370 | * @return Address 371 | */ 372 | public function getAddress() : ?Address 373 | { 374 | return $this->address; 375 | } 376 | } 377 | 378 | // ... some place else, in your application ... // 379 | 380 | // Data for your Person DTO 381 | $data = [ 382 | 'name' => 'Arial Jackson', 383 | 'age' => 42, 384 | 385 | // Notice that we are NOT passing in an instance of Address, but an array instead! 386 | 'address' => [ 387 | 'street' => 'Somewhere str. 44' 388 | ] 389 | ]; 390 | 391 | $person = new Person($data); 392 | $address = $person->getAddress(); // Instance of Address - Will automatically be resolved (if possible). 393 | ``` 394 | 395 | In the above example, [Laravel's Service Container](http://laravel.com/docs/5.5/container) attempts to find and create any concrete instances that are expected. 396 | 397 | Furthermore, the default DTO abstraction (`Aedart\DTO\DataTransferObject`) will attempt to automatically populate that instance. 398 | 399 | ### Interface bindings 400 | 401 | If you prefer to use interfaces instead, then you need to `bind` those interfaces to concrete instances, before the DTOs / service container can handle and resolve them. 402 | 403 | #### Outside Laravel Application 404 | 405 | If you are outside a Laravel application, then you can bind interfaces to concrete instances, in the following way; 406 | 407 | ```php 408 | bind(CityInterface::class, function($app){ 419 | return new City(); 420 | }); 421 | ``` 422 | 423 | #### Inside Laravel Application 424 | 425 | Inside your application's [service provider](https://laravel.com/docs/5.5/providers) (or perhaps a custom service provider), you can bind your DTO interfaces to concrete instances; 426 | 427 | ```php 428 | app->bind(CityInterface::class, function($app){ 434 | return new City(); 435 | }); 436 | ``` 437 | #### Example 438 | 439 | Given that you have bound your interfaces to concrete instances, then the following is possible 440 | 441 | ```php 442 | city = $city; 490 | } 491 | 492 | /** 493 | * Get the city 494 | * 495 | * @return CityInterface 496 | */ 497 | public function getCity() : ?CityInterface 498 | { 499 | return $this->city; 500 | } 501 | } 502 | 503 | // ... some other place in your application ... // 504 | 505 | $addressData = [ 506 | 'street' => 'Marshall Street 27', 507 | 'city' => [ 508 | 'name' => 'Lincoln' 509 | ] 510 | ]; 511 | 512 | // Create new instance and populate 513 | $address = new Address($addressData); // Will attempt to automatically resolve the expected city property, 514 | // of the CityInterface type, by creating a concrete City, using 515 | // the service container, and resolve the bound interface instance 516 | ``` 517 | 518 | ## Contribution 519 | 520 | Have you found a defect ( [bug or design flaw](https://en.wikipedia.org/wiki/Software_bug) ), or do you wish improvements? In the following sections, you might find some useful information 521 | on how you can help this project. In any case, I thank you for taking the time to help me improve this project's deliverables and overall quality. 522 | 523 | ### Bug Report 524 | 525 | If you are convinced that you have found a bug, then at the very least you should create a new issue. In that given issue, you should as a minimum describe the following; 526 | 527 | * Where is the defect located 528 | * A good, short and precise description of the defect (Why is it a defect) 529 | * How to replicate the defect 530 | * (_A possible solution for how to resolve the defect_) 531 | 532 | When time permits it, I will review your issue and take action upon it. 533 | 534 | ### Fork, code and send pull-request 535 | 536 | A good and well written bug report can help me a lot. Nevertheless, if you can or wish to resolve the defect by yourself, here is how you can do so; 537 | 538 | * Fork this project 539 | * Create a new local development branch for the given defect-fix 540 | * Write your code / changes 541 | * Create executable test-cases (prove that your changes are solid!) 542 | * Commit and push your changes to your fork-repository 543 | * Send a pull-request with your changes 544 | * _Drink a [Beer](https://en.wikipedia.org/wiki/Beer) - you earned it_ :) 545 | 546 | As soon as I receive the pull-request (_and have time for it_), I will review your changes and merge them into this project. If not, I will inform you why I choose not to. 547 | 548 | ## Acknowledgement 549 | 550 | * [Martin Fowler](http://martinfowler.com/aboutMe.html), for sharing his knowledge about [DTOs](http://martinfowler.com/eaaCatalog/dataTransferObject.html) and many other design patterns 551 | * [Taylor Otwell](https://github.com/taylorotwell), for creating [Laravel](https://laravel.com) and especially the [Service Container](https://laravel.com/docs/5.4/container), that I'm using daily 552 | * [Jeffrey Way](https://github.com/JeffreyWay), for creating [Laracasts](https://laracasts.com/) - a great place to learn new things... And where I finally understood some of the principles of IoC! 553 | 554 | ## Versioning 555 | 556 | This package follows [Semantic Versioning 2.0.0](http://semver.org/) 557 | 558 | ## License 559 | 560 | [BSD-3-Clause](http://spdx.org/licenses/BSD-3-Clause), Read the LICENSE file included in this package 561 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | support: tests/_support 7 | envs: tests/_envs 8 | settings: 9 | bootstrap: _bootstrap.php 10 | colors: true 11 | memory_limit: 1024M 12 | extensions: 13 | enabled: 14 | - Codeception\Extension\RunFailed 15 | coverage: 16 | enabled: true 17 | whitelist: 18 | include: 19 | - src/* 20 | exclude: 21 | - tests/* 22 | - vendor/* 23 | modules: 24 | config: 25 | Db: 26 | dsn: '' 27 | user: '' 28 | password: '' 29 | dump: tests/_data/dump.sql 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "abandoned": "aedart/athenaeum", 3 | "name": "aedart/dto", 4 | "description": "A variation / interpretation of the Data Transfer Object (DTO) design pattern (Distribution Pattern). Provides an abstraction for such DTOs", 5 | "keywords": [ 6 | "Data Transfer Object", 7 | "DTO", 8 | "Design Pattern" 9 | ], 10 | "homepage": "https://github.com/aedart/data-transfer-object-dto", 11 | "license": "BSD-3-Clause", 12 | "type": "library", 13 | "authors": [ 14 | { 15 | "name": "Alin Eugen Deac", 16 | "email": "aedart@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.1.0", 21 | "aedart/overload": "~5.0", 22 | "aedart/util": "~5.0", 23 | "illuminate/container": "5.6.*", 24 | "illuminate/support": "5.6.*" 25 | }, 26 | "require-dev": { 27 | "aedart/license": "1.*", 28 | "aedart/license-file-manager": "~2.0", 29 | "aedart/testing": "~2.0", 30 | "symfony/var-dumper": "~4.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Aedart\\DTO\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "classmap": [ 39 | "tests/testCases/" 40 | ] 41 | }, 42 | "minimum-stability": "stable", 43 | "prefer-stable": true, 44 | "scripts": { 45 | "post-update-cmd": "vendor/bin/license-manager license:copy vendor/aedart/license/aedart/BSD-3-Clause" 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "5.1.x-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Contracts/DataTransferObject.php: -------------------------------------------------------------------------------- 1 | Data Transfer Object 14 | * 15 | * Variation / Interpretation of the Data Transfer Object (DTO) design pattern (Distribution Pattern). 16 | * A DTO is responsible for; 17 | * 18 | * 23 | * 24 | *

Getters and Setters

25 | * 26 | * This DTO ensures that its belonging properties / attributes can be overloaded, 27 | * if those properties / attributes have corresponding getters and setters (accessors and mutators). 28 | * 29 | *

Serialization

30 | * 31 | * In this variation of the DTO, serialization defaults to Json. 32 | * 33 | *

Inversion of Control / Dependency Inversion 34 | * 35 | * Each DTO holds an instance of a Inversion of Control (IoC) service container, which can be 36 | * used for resolving nested dependencies, when populating the DTO with data. E.g. when a DTO's property is a 37 | * class object instance type. However, this is implementation specific. 38 | * 39 | *

When to use DTOs

40 | * 41 | * 45 | * 46 | * There are probably many more reasons why and when you should use DTOs. However, you should know that using DTOs can / will 47 | * increase complexity of your project! 48 | * 49 | * @see http://martinfowler.com/eaaCatalog/dataTransferObject.html 50 | * @see https://en.wikipedia.org/wiki/Data_transfer_object 51 | * @see https://en.wikipedia.org/wiki/Mutator_method 52 | * @see http://php.net/manual/en/language.oop5.overloading.php 53 | * @see http://php.net/manual/en/class.arrayaccess.php 54 | * @see https://en.wikipedia.org/wiki/Inversion_of_control 55 | * @see http://laravel.com/docs/5.1/container 56 | * @see http://php.net/manual/en/class.jsonserializable.php 57 | * 58 | * @author Alin Eugen Deac 59 | * @package Aedart\DTO\Contracts 60 | */ 61 | interface DataTransferObject extends ArrayAccess, 62 | Arrayable, 63 | Jsonable, 64 | JsonSerializable, 65 | Populatable 66 | { 67 | /** 68 | * Returns a list of the properties / attributes that 69 | * this Data Transfer Object can be populated with 70 | * 71 | * @return string[] 72 | */ 73 | public function populatableProperties() : array; 74 | 75 | /** 76 | * Returns the container that is responsible for 77 | * resolving dependency injection or eventual 78 | * nested object 79 | * 80 | * @return Container|null IoC service Container or null if none defined 81 | */ 82 | public function container() : ?Container; 83 | } 84 | -------------------------------------------------------------------------------- /src/DataTransferObject.php: -------------------------------------------------------------------------------- 1 | Abstract Data Transfer Object 19 | * 20 | * This DTO abstraction offers default implementation of the following; 21 | * 22 | * 30 | * 31 | * @see \Aedart\DTO\Contracts\DataTransferObject 32 | * 33 | * @author Alin Eugen Deac 34 | * @package Aedart\DTO 35 | */ 36 | abstract class DataTransferObject implements DataTransferObjectInterface 37 | { 38 | use PropertyOverloadTrait { 39 | __set as __setFromTrait; 40 | } 41 | 42 | /** 43 | * Container that must resolve 44 | * dependency injection, should it be 45 | * needed 46 | * 47 | * @var Container|null The IoC service container 48 | */ 49 | private $ioc = null; 50 | 51 | /** 52 | * Create a new instance of this Data Transfer Object 53 | * 54 | *
55 | * 56 | * IoC Service Container: If no container is provided, a default 57 | * service container is attempted to be resolved, using a application 58 | * facade. 59 | * 60 | * @see \Aedart\DTO\Providers\Bootstrap 61 | * @see \Illuminate\Contracts\Container\Container 62 | * @see http://laravel.com/docs/5.1/container#introduction 63 | * 64 | * @param array $data [optional] This object's properties / attributes 65 | * @param Container $container [optional] Eventual container that is responsible for resolving dependency injection 66 | */ 67 | public function __construct(array $data = [], ?Container $container = null) 68 | { 69 | $this->ioc = $container; 70 | 71 | $this->populate($data); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function container() : ?Container 78 | { 79 | if( ! isset($this->ioc)){ 80 | $this->ioc = App::getFacadeApplication(); 81 | } 82 | 83 | return $this->ioc; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function __set(string $name, $value) 90 | { 91 | $resolvedValue = $value; 92 | 93 | $methodName = $this->generateSetterName($name); 94 | if ($this->hasInternalMethod($methodName)) { 95 | $resolvedValue = $this->resolveValue($methodName, $value); 96 | } 97 | 98 | $this->__setFromTrait($name, $resolvedValue); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function populatableProperties() : array 105 | { 106 | $reflection = new ReflectionClass($this); 107 | 108 | $properties = $reflection->getProperties(); 109 | 110 | $output = []; 111 | 112 | foreach ($properties as $reflectionProperty) { 113 | $name = $reflectionProperty->getName(); 114 | $getterMethod = $this->generateGetterName($name); 115 | 116 | if ($this->hasInternalMethod($getterMethod)) { 117 | $output[] = $name; 118 | } 119 | } 120 | 121 | return $output; 122 | } 123 | 124 | /** 125 | * {@inheritdoc} 126 | */ 127 | public function populate(array $data = []) : void 128 | { 129 | foreach ($data as $name => $value) { 130 | $this->__set($name, $value); 131 | } 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function toArray() 138 | { 139 | 140 | $properties = $this->populatableProperties(); 141 | $output = []; 142 | 143 | foreach ($properties as $property) { 144 | // Make sure that property is not unset 145 | if (!isset($this->$property)) { 146 | continue; 147 | } 148 | 149 | $output[$property] = $this->__get($property); 150 | } 151 | 152 | return $output; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function offsetExists($offset) 159 | { 160 | return isset($this->$offset); 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function offsetGet($offset) 167 | { 168 | return $this->$offset; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function offsetSet($offset, $value) 175 | { 176 | $this->$offset = $value; 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function offsetUnset($offset) 183 | { 184 | unset($this->$offset); 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function toJson($options = 0) 191 | { 192 | return json_encode($this->jsonSerialize(), $options); 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | function jsonSerialize() 199 | { 200 | return $this->toArray(); 201 | } 202 | 203 | /** 204 | * Returns a string representation of this Data Transfer Object 205 | * 206 | * @return string String representation of this data transfer object 207 | */ 208 | public function __toString() 209 | { 210 | return $this->toJson(); 211 | } 212 | 213 | /** 214 | * Method is invoked by `var_dump()` 215 | * 216 | *
217 | * 218 | * By default, this method will NOT display the `_propertyAccessibilityLevel` 219 | * property. This property is an internal behavioural modifier (state), 220 | * that should not be used, unless very important / special case. 221 | * 222 | * @see \Aedart\Overload\Traits\Helper\PropertyAccessibilityTrait 223 | * 224 | * @return array All the available properties of this Data Transfer Object 225 | */ 226 | public function __debugInfo() : array 227 | { 228 | return $this->toArray(); 229 | } 230 | 231 | /*********************************************************************** 232 | * Internal Methods 233 | **********************************************************************/ 234 | 235 | /** 236 | * Resolve and return the given value, for the given setter method 237 | * 238 | * @param string $setterMethodName The setter method to be invoked 239 | * @param mixed $value The value to be passed to the setter method 240 | * 241 | * @return mixed 242 | */ 243 | protected function resolveValue(string $setterMethodName, $value) 244 | { 245 | $reflection = new ReflectionClass($this); 246 | 247 | $method = $reflection->getMethod($setterMethodName); 248 | 249 | $parameter = $method->getParameters()[0]; 250 | 251 | return $this->resolveParameter($parameter, $value); 252 | } 253 | 254 | /** 255 | * Resolve the given parameter; pass the given value to it 256 | * 257 | * @param ReflectionParameter $parameter The setter method's parameter reflection 258 | * @param mixed $value The value to be passed to the setter method 259 | * 260 | * @return mixed 261 | * @throws BindingResolutionException a) If no concrete instance could be resolved from the IoC, or 262 | * b) If the instance is not populatable and or the given value is not an 263 | * array that can be passed to the populatable instance 264 | * c) No service container is available 265 | */ 266 | protected function resolveParameter(ReflectionParameter $parameter, $value) 267 | { 268 | // If there is no class for the given parameter 269 | // then some kind of primitive data has been provided 270 | // and thus we need only to return it. 271 | $paramClass = $parameter->getClass(); 272 | if ( ! isset($paramClass)) { 273 | return $value; 274 | } 275 | 276 | // Fetch the name of the class 277 | $className = $paramClass->getName(); 278 | 279 | // If the value corresponds to the given expected class, 280 | // then there is no need to resolve anything from the 281 | // IoC service container. 282 | if ($value instanceof $className) { 283 | return $value; 284 | } 285 | 286 | $container = $this->container(); 287 | 288 | // Fail if no service container is available 289 | if (is_null($container)) { 290 | $message = sprintf( 291 | 'No IoC service container is available, cannot resolve property "%s" of the type "%s"; do not know how to populate with "%s"', 292 | $parameter->getName(), 293 | $className, 294 | var_export($value, true) 295 | ); 296 | throw new BindingResolutionException($message); 297 | } 298 | 299 | // Get the resolved instance for the IoC container 300 | $instance = $container->make($className); 301 | 302 | // From Laravel 5.4, the Container::make method no longer accepts 303 | // parameters, which is really sad... Nevertheless, we attempt to 304 | // resolve this, simply by checking if the instance can be 305 | // populated with the given value, which is exactly what the 306 | // `resolveInstancePopulation` method does. 307 | return $this->resolveInstancePopulation($instance, $parameter, $value); 308 | } 309 | 310 | /** 311 | * Attempts to populate instance, if possible 312 | * 313 | * @param object $instance The instance that must be populated 314 | * @param ReflectionParameter $parameter Setter method's parameter reflection that requires the given instance 315 | * @param mixed $value The value to be passed to the setter method 316 | * 317 | * @return mixed 318 | * @throws BindingResolutionException If the instance is not populatable and or the given value is not an 319 | * array that can be passed to the populatable instance 320 | */ 321 | protected function resolveInstancePopulation($instance, ReflectionParameter $parameter, $value) 322 | { 323 | 324 | // Check if instance is populatable and if the given value 325 | // is an array. 326 | if ($instance instanceof Populatable && is_array($value)) { 327 | $instance->populate($value); 328 | 329 | return $instance; 330 | } 331 | 332 | // If we reach this part, then we are simply going to fail. 333 | // It is NOT safe to continue and make assumptions on how 334 | // we can populate the given instance. For this reason, we 335 | // just throw an exception 336 | $message = sprintf( 337 | 'Unable to resolve dependency for property "%s" of the type "%s"; do not know how to populate with "%s"', 338 | $parameter->getName(), 339 | $parameter->getClass()->getName(), 340 | var_export($value, true) 341 | ); 342 | 343 | throw new BindingResolutionException($message); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/Providers/Bootstrap.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * Boots a Inversion of Control (IoC) Container, that is responsible 15 | * for dealing with dependency injection. 16 | * 17 | *
18 | * 19 | * Warning This class is ONLY needed if you are using 20 | * this package outside a Laravel Framework. 21 | * 22 | * @see https://en.wikipedia.org/wiki/Inversion_of_control 23 | * @see http://laravel.com/docs/5.1/container#introduction 24 | * 25 | * @author Alin Eugen Deac 26 | * @package Aedart\DTO\Providers 27 | */ 28 | class Bootstrap 29 | { 30 | 31 | /** 32 | * The IoC Service 33 | * 34 | * @var \Illuminate\Contracts\Container\Container 35 | */ 36 | protected static $container = null; 37 | 38 | /** 39 | * Boots the Inversion of Control (IoC) Container 40 | */ 41 | public static function boot() : void 42 | { 43 | $container = self::getContainer(); 44 | $container->singleton('app', $container); 45 | 46 | Facade::setFacadeApplication($container); 47 | } 48 | 49 | /** 50 | * Destroy the Inversion of Control (IoC) Container 51 | */ 52 | public static function destroy() : void 53 | { 54 | Facade::clearResolvedInstances(); 55 | 56 | Facade::setFacadeApplication(null); 57 | 58 | self::setContainer(null); 59 | } 60 | 61 | /** 62 | * Get the IoC service container 63 | * 64 | * If no IoC was set, this method will set and 65 | * return a default container 66 | * 67 | * @see getDefaultContainer 68 | * 69 | * @return \Illuminate\Contracts\Container\Container|null 70 | */ 71 | public static function getContainer() : ?ContainerInterface 72 | { 73 | if (is_null(self::$container)) { 74 | self::setContainer(self::getDefaultContainer()); 75 | } 76 | 77 | return self::$container; 78 | } 79 | 80 | /** 81 | * Set the IoC service container 82 | * 83 | * Info: You should invoke `boot()` after setting a 84 | * new container 85 | * 86 | * @param \Illuminate\Contracts\Container\Container|null $container [optional] 87 | */ 88 | public static function setContainer(?ContainerInterface $container = null) : void 89 | { 90 | self::$container = $container; 91 | } 92 | 93 | /** 94 | * Returns a default IoC service container 95 | * 96 | * @return \Illuminate\Contracts\Container\Container|null 97 | */ 98 | public static function getDefaultContainer() : ?ContainerInterface 99 | { 100 | return new Container(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Address extends DataTransferObject 17 | { 18 | /** 19 | * @var string 20 | */ 21 | protected $street = ''; 22 | 23 | /** 24 | * @var City 25 | */ 26 | protected $city = null; 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getStreet() : ?string 32 | { 33 | return $this->street; 34 | } 35 | 36 | /** 37 | * @param string $street 38 | */ 39 | public function setStreet(?string $street) 40 | { 41 | $this->street = $street; 42 | } 43 | 44 | /** 45 | * @return City 46 | */ 47 | public function getCity() : ?City 48 | { 49 | return $this->city; 50 | } 51 | 52 | /** 53 | * @param City|null $city 54 | */ 55 | public function setCity(?City $city) 56 | { 57 | $this->city = $city; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /tests/testCases/helpers/BadUnpopulatableObject.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class BadUnpopulatableObject 16 | { 17 | 18 | /** 19 | * @var string 20 | */ 21 | protected $foo = ''; 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getFoo() : string 27 | { 28 | return $this->foo; 29 | } 30 | 31 | /** 32 | * @param string $foo 33 | */ 34 | public function setFoo(string $foo) 35 | { 36 | $this->foo = $foo; 37 | } 38 | } -------------------------------------------------------------------------------- /tests/testCases/helpers/City.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class City extends DataTransferObject 17 | { 18 | 19 | protected $name = ''; 20 | 21 | protected $zipCode = 0; 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getName() : string 27 | { 28 | return $this->name; 29 | } 30 | 31 | /** 32 | * @param string $name 33 | */ 34 | public function setName(string $name) 35 | { 36 | $this->name = $name; 37 | } 38 | 39 | /** 40 | * @return int 41 | */ 42 | public function getZipCode() : int 43 | { 44 | return $this->zipCode; 45 | } 46 | 47 | /** 48 | * @param int $zipCode 49 | */ 50 | public function setZipCode(int $zipCode) 51 | { 52 | $this->zipCode = $zipCode; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /tests/testCases/helpers/Contracts/NotesInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface NotesInterface 11 | { 12 | /** 13 | * @param string[] $notes 14 | * 15 | * @return void 16 | */ 17 | public function setNotes(array $notes) : void; 18 | 19 | /** 20 | * @return string[] 21 | */ 22 | public function getNotes() : array; 23 | } -------------------------------------------------------------------------------- /tests/testCases/helpers/Notes.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Notes extends DataTransferObject implements NotesInterface 16 | { 17 | /** 18 | * @var string[] 19 | */ 20 | protected $notes = []; 21 | 22 | /** 23 | * @param string[] $notes 24 | */ 25 | public function setNotes(array $notes) : void 26 | { 27 | $this->notes = $notes; 28 | } 29 | 30 | /** 31 | * @return string[] 32 | */ 33 | public function getNotes() : array 34 | { 35 | return $this->notes; 36 | } 37 | } -------------------------------------------------------------------------------- /tests/testCases/helpers/Person.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Person extends DataTransferObject 20 | { 21 | 22 | protected $name = ''; 23 | 24 | /** 25 | * @var Address 26 | */ 27 | protected $address = null; 28 | 29 | /** 30 | * @var NotesInterface 31 | */ 32 | protected $notes = null; 33 | 34 | /** 35 | * @var BadUnpopulatableObject 36 | */ 37 | protected $badInstance = null; 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getName() : string 43 | { 44 | return $this->name; 45 | } 46 | 47 | /** 48 | * @param string $name 49 | */ 50 | public function setName(string $name) 51 | { 52 | $this->name = $name; 53 | } 54 | 55 | /** 56 | * @return Address|null 57 | */ 58 | public function getAddress() : ?Address 59 | { 60 | return $this->address; 61 | } 62 | 63 | /** 64 | * @param Address|null $address 65 | */ 66 | public function setAddress(?Address $address) 67 | { 68 | $this->address = $address; 69 | } 70 | 71 | /** 72 | * @return NotesInterface|null 73 | */ 74 | public function getNotes() : ?NotesInterface 75 | { 76 | return $this->notes; 77 | } 78 | 79 | /** 80 | * @param NotesInterface|null $notes 81 | */ 82 | public function setNotes(?NotesInterface $notes) 83 | { 84 | $this->notes = $notes; 85 | } 86 | 87 | /** 88 | * @return BadUnpopulatableObject|null 89 | */ 90 | public function getBadInstance() : ?BadUnpopulatableObject 91 | { 92 | return $this->badInstance; 93 | } 94 | 95 | /** 96 | * @param BadUnpopulatableObject|null $badInstance 97 | */ 98 | public function setBadInstance(?BadUnpopulatableObject $badInstance) 99 | { 100 | $this->badInstance = $badInstance; 101 | } 102 | } -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit (internal) tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit -------------------------------------------------------------------------------- /tests/unit/BootstrapTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class BootstrapTest extends UnitTestCase 17 | { 18 | 19 | /*************************************************************** 20 | * Utilities and helpers 21 | **************************************************************/ 22 | 23 | /** 24 | * Get a Dto mock 25 | * 26 | * @return DataTransferObject 27 | */ 28 | protected function getDto() 29 | { 30 | return new City(); 31 | } 32 | 33 | /*************************************************************** 34 | * Actual tests 35 | **************************************************************/ 36 | 37 | /** 38 | * @test 39 | */ 40 | public function canBoot() 41 | { 42 | try { 43 | Bootstrap::boot(); 44 | 45 | $this->assertInstanceOf(Container::class, Bootstrap::getContainer()); 46 | } catch (Exception $e) { 47 | $this->fail('Could not boot; ' . PHP_EOL . $e); 48 | } 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function hasSetFacadeApplication() 55 | { 56 | $this->assertInstanceOf(Container::class, App::getFacadeApplication(), 57 | 'App Facade should have a container set'); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function dtoHasContainerSet() 64 | { 65 | $dto = $this->getDto(); 66 | 67 | $this->assertInstanceOf(Container::class, $dto->container(), 'Invalid container on DTO'); 68 | } 69 | 70 | /** 71 | * @test 72 | * 73 | * @depends hasSetFacadeApplication 74 | */ 75 | public function canDestroy() 76 | { 77 | try { 78 | Bootstrap::destroy(); 79 | } catch (Exception $e) { 80 | $this->fail('Could not destroy; ' . PHP_EOL . $e); 81 | } 82 | } 83 | 84 | /** 85 | * @test 86 | * 87 | * @depends canDestroy 88 | */ 89 | public function hasUnsetFacadeApplication() 90 | { 91 | $this->assertNull(App::getFacadeApplication(), 'Container / Application should be null'); 92 | } 93 | 94 | /** 95 | * @test 96 | * 97 | * @depends hasUnsetFacadeApplication 98 | */ 99 | public function dtoHasNoContainerSet() 100 | { 101 | $dto = $this->getDto(); 102 | 103 | $this->assertNull($dto->container(), 'No container should be available'); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/unit/DataTransferObjectTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DataTransferObjectTest extends UnitTestCase 15 | { 16 | 17 | /*************************************************************** 18 | * Utilities and helpers 19 | **************************************************************/ 20 | 21 | /*************************************************************** 22 | * Actual tests 23 | **************************************************************/ 24 | 25 | /** 26 | * @test 27 | * @covers ::__construct 28 | * @covers ::populate 29 | */ 30 | public function canCreateInstanceWithoutArguments() 31 | { 32 | try { 33 | $dto = new DummyDto(); 34 | 35 | $this->assertTrue(true, 'Instance created'); 36 | } catch (Exception $e) { 37 | $this->fail('Cannot create instance without arguments;' . PHP_EOL . $e); 38 | } 39 | } 40 | 41 | /** 42 | * @test 43 | * @covers ::__construct 44 | * @covers ::populate 45 | */ 46 | public function canCreateInstanceWithArguments() 47 | { 48 | $data = [ 49 | 'age' => $this->faker->randomDigit 50 | ]; 51 | 52 | try { 53 | $dto = new DummyDto($data); 54 | 55 | $this->assertTrue(true, 'Instance created'); 56 | } catch (Exception $e) { 57 | $this->fail('Cannot create instance with arguments;' . PHP_EOL . $e); 58 | } 59 | } 60 | 61 | /** 62 | * @test 63 | * @covers ::populatableProperties 64 | */ 65 | public function canObtainPopulatableProperties() 66 | { 67 | $dto = new DummyDto(); 68 | 69 | $populatableProperties = $dto->populatableProperties(); 70 | 71 | Debug::debug($populatableProperties); 72 | 73 | $expectedList = ['name', 'age']; 74 | 75 | $this->assertInternalType('array', $populatableProperties, 'Array was expected'); 76 | $this->assertSame($expectedList, $populatableProperties, 'Invalid list of populatable properties returned'); 77 | } 78 | 79 | /** 80 | * @test 81 | * @covers ::populate 82 | */ 83 | public function hasPopulatedCorrectly() 84 | { 85 | $data = [ 86 | 'age' => $this->faker->randomDigit, 87 | 'name' => $this->faker->name 88 | ]; 89 | 90 | $dto = new DummyDto($data); 91 | 92 | $this->assertSame($data['name'], $dto->name, 'Name is incorrect'); 93 | $this->assertSame($data['age'], $dto->age, 'Age is incorrect'); 94 | } 95 | 96 | /** 97 | * @test 98 | * @covers ::offsetExists 99 | */ 100 | public function canDetermineIfOffsetExistsOrNot() 101 | { 102 | $dto = new DummyDto(); 103 | 104 | $this->assertTrue(isset($dto['name']), 'Name exists'); 105 | $this->assertTrue(isset($dto['age']), 'Age exists'); 106 | $this->assertFalse(isset($dto['unknownProperty']), 'Unknown property should NOT Exist'); 107 | } 108 | 109 | /** 110 | * @test 111 | * @covers ::offsetGet 112 | */ 113 | public function canGetViaOffset() 114 | { 115 | $data = [ 116 | 'age' => $this->faker->randomDigit, 117 | 'name' => $this->faker->name 118 | ]; 119 | 120 | $dto = new DummyDto($data); 121 | 122 | $this->assertSame($data['name'], $dto['name'], 'Could not get "name" via offset'); 123 | } 124 | 125 | /** 126 | * @test 127 | * @covers ::offsetGet 128 | * 129 | * @expectedException \Aedart\Overload\Exception\UndefinedPropertyException 130 | */ 131 | public function failsWhenOffsetDoesNotExist() 132 | { 133 | $dto = new DummyDto(); 134 | 135 | $something = $dto['offsetThatDoesNotExist']; 136 | } 137 | 138 | /** 139 | * @test 140 | * @covers ::offsetSet 141 | */ 142 | public function canSetViaOffset() 143 | { 144 | $dto = new DummyDto(); 145 | 146 | $name = $this->faker->name; 147 | 148 | $dto['name'] = $name; 149 | 150 | $this->assertSame($name, $dto->name, 'Name was not set correctly via offset'); 151 | } 152 | 153 | /** 154 | * @test 155 | * @covers ::offsetSet 156 | * 157 | * @expectedException \Aedart\Overload\Exception\UndefinedPropertyException 158 | */ 159 | public function failsSettingViaOffsetAndPropertyDoesNotExist() 160 | { 161 | $dto = new DummyDto(); 162 | 163 | $dto['offsetThatDoesNotExist'] = $this->faker->address; 164 | } 165 | 166 | /** 167 | * @test 168 | * @covers ::offsetUnset 169 | */ 170 | public function canUnsetViaOffset() 171 | { 172 | $data = [ 173 | 'age' => $this->faker->randomDigit, 174 | 'name' => $this->faker->name 175 | ]; 176 | 177 | $dto = new DummyDto($data); 178 | 179 | unset($dto['age']); 180 | 181 | $this->assertFalse(isset($dto['age']), 'Age should be unset and no longer available'); 182 | } 183 | 184 | /** 185 | * @test 186 | * @covers ::offsetUnset 187 | * @covers ::offsetSet 188 | */ 189 | public function reassignValueToUnsetProperty() 190 | { 191 | $data = [ 192 | 'age' => $this->faker->randomDigit, 193 | 'name' => $this->faker->name 194 | ]; 195 | 196 | $dto = new DummyDto($data); 197 | 198 | unset($dto['age']); 199 | 200 | $newAge = $this->faker->randomDigit; 201 | 202 | $dto['age'] = $newAge; 203 | 204 | $this->assertSame($newAge, $dto->age, 'Age should have a new value'); 205 | } 206 | 207 | /** 208 | * @test 209 | * @covers ::jsonSerialize 210 | */ 211 | public function returnsDataThatCanBeSerialisedToJson() 212 | { 213 | $data = [ 214 | 'age' => $this->faker->randomDigit, 215 | 'name' => $this->faker->name 216 | ]; 217 | 218 | $dto = new DummyDto($data); 219 | 220 | $serialised = json_encode($dto); 221 | 222 | $this->assertNotFalse($serialised, 'Could not serialise to json'); 223 | $this->assertJson($serialised, 'Serialised data is NOT json'); 224 | } 225 | 226 | /** 227 | * @test 228 | * @covers ::toJson 229 | */ 230 | public function canSerialiseToJson() 231 | { 232 | $data = [ 233 | 'age' => $this->faker->randomDigit, 234 | 'name' => $this->faker->name 235 | ]; 236 | 237 | $dto = new DummyDto($data); 238 | 239 | $this->assertJson($dto->toJson(), 'Could not serialise to json'); 240 | } 241 | 242 | /** 243 | * @test 244 | * @covers ::__toString 245 | */ 246 | public function canGetStringRepresentationOfDto() 247 | { 248 | $data = [ 249 | 'age' => $this->faker->randomDigit, 250 | 'name' => $this->faker->name 251 | ]; 252 | 253 | $dto = new DummyDto($data); 254 | 255 | $this->assertInternalType('string', $dto->__toString(), 'toString does NOT return a string!'); 256 | } 257 | 258 | /** 259 | * @test 260 | * @covers ::__debugInfo 261 | * @covers ::toArray 262 | */ 263 | public function debugInformationDoesNotContainSpecialProperty() 264 | { 265 | $dto = new DummyDto(); 266 | 267 | $debugInformation = $dto->__debugInfo(); 268 | 269 | Debug::debug($debugInformation); 270 | 271 | $keys = array_keys($debugInformation); 272 | 273 | $this->assertNotContains('_propertyAccessibilityLevel', $keys); 274 | } 275 | 276 | /** 277 | * @test 278 | * @covers ::__debugInfo 279 | * @covers ::toArray 280 | */ 281 | public function debugInformationDoesNotContainUnsetProperties() 282 | { 283 | $data = [ 284 | 'age' => $this->faker->randomDigit, 285 | 'name' => $this->faker->name 286 | ]; 287 | 288 | $dto = new DummyDto($data); 289 | 290 | unset($dto->name); 291 | 292 | $debugInformation = $dto->__debugInfo(); 293 | 294 | Debug::debug($debugInformation); 295 | 296 | $keys = array_keys($debugInformation); 297 | 298 | $this->assertNotContains('name', $keys); 299 | } 300 | } 301 | 302 | /** 303 | * Class Dummy Dto 304 | * 305 | * A dummy class, that extends the Data Transfer Object abstraction 306 | * 307 | * @property string $name 308 | * @property int $age 309 | * 310 | * @author Alin Eugen Deac 311 | */ 312 | class DummyDto extends DataTransferObject 313 | { 314 | 315 | protected $name = ''; 316 | 317 | protected $age = 0; 318 | 319 | /** 320 | * @return string 321 | */ 322 | public function getName() 323 | { 324 | return $this->name; 325 | } 326 | 327 | /** 328 | * @param string $name 329 | */ 330 | public function setName($name) 331 | { 332 | $this->name = $name; 333 | } 334 | 335 | /** 336 | * @return int 337 | */ 338 | public function getAge() 339 | { 340 | return $this->age; 341 | } 342 | 343 | /** 344 | * @param int $age 345 | */ 346 | public function setAge($age) 347 | { 348 | $this->age = $age; 349 | } 350 | 351 | } -------------------------------------------------------------------------------- /tests/unit/NestedDataTransferObjectTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class NestedDataTransferObjectTest extends UnitTestCase 16 | { 17 | 18 | protected function _before() 19 | { 20 | parent::_before(); 21 | 22 | Bootstrap::boot(); 23 | } 24 | 25 | protected function _after() 26 | { 27 | Bootstrap::destroy(); 28 | 29 | parent::_after(); 30 | } 31 | 32 | /*************************************************************** 33 | * Utilities and helpers 34 | **************************************************************/ 35 | 36 | /** 37 | * Get the IoC service container 38 | * 39 | * @return Container|null 40 | */ 41 | protected function getContainer() : ?Container 42 | { 43 | return Bootstrap::getContainer(); 44 | } 45 | 46 | /*************************************************************** 47 | * Actual tests 48 | **************************************************************/ 49 | 50 | /** 51 | * @test 52 | */ 53 | public function canPopulatePropertyOfPrimitiveType() 54 | { 55 | $data = [ 56 | 'name' => $this->faker->name 57 | ]; 58 | 59 | $person = new Person($data); 60 | 61 | $this->assertSame($data['name'], $person->name, 'Name should had been set'); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function canPopulateWithNestedObjectInstance() 68 | { 69 | $cityData = [ 70 | 'name' => $this->faker->city, 71 | 'zipCode' => (int) $this->faker->postcode 72 | ]; 73 | 74 | $addressData = [ 75 | 'street' => $this->faker->streetName, 76 | 'city' => new City($cityData) 77 | ]; 78 | 79 | $personData = [ 80 | 'name' => $this->faker->name, 81 | 'address' => new Address($addressData) 82 | ]; 83 | 84 | $person = new Person($personData); 85 | 86 | $this->assertSame($addressData['street'], $person->address->street, 'Street should have been set'); 87 | $this->assertSame($cityData['name'], $person->address->city->name, 'City name should have been set'); 88 | } 89 | 90 | /** 91 | * Please note: because we are using Laravel's default container, it 92 | * can handle creating instances of concrete classes that are expected. 93 | * Thus, there is no need to `bind` them, in this case 94 | * 95 | * @test 96 | */ 97 | public function canResolveAndPopulateUnboundConcreteInstances() 98 | { 99 | $personData = [ 100 | 'name' => $this->faker->name, 101 | 'address' => [ 102 | 'street' => $this->faker->streetName, 103 | 'city' => [ 104 | 'name' => $this->faker->city, 105 | 'zipCode' => (int) $this->faker->postcode 106 | ] 107 | ] 108 | ]; 109 | 110 | $person = new Person($personData); 111 | 112 | $this->assertSame($personData['name'], $person->name, 'Name of person is invalid'); 113 | $this->assertSame($personData['address']['street'], $person->address->street, 'Street should have been set'); 114 | $this->assertSame($personData['address']['city']['name'], $person->address->city->name, 'City name should have been set'); 115 | } 116 | 117 | /** 118 | * @test 119 | * 120 | * @expectedException \Illuminate\Contracts\Container\BindingResolutionException 121 | */ 122 | public function failWhenNoServiceContainerIsAvailable() 123 | { 124 | Bootstrap::destroy(); 125 | 126 | $personData = [ 127 | 'name' => $this->faker->name, 128 | 'address' => [ 129 | 'street' => $this->faker->streetName, 130 | 'city' => [ 131 | 'name' => $this->faker->city, 132 | 'zipCode' => (int) $this->faker->postcode 133 | ] 134 | ] 135 | ]; 136 | 137 | $person = new Person($personData); 138 | } 139 | 140 | /** 141 | * @test 142 | */ 143 | public function canSerialiseNestedInstances() 144 | { 145 | $personData = [ 146 | 'name' => $this->faker->name, 147 | 'address' => [ 148 | 'street' => $this->faker->streetName, 149 | 'city' => [ 150 | 'name' => $this->faker->city, 151 | 'zipCode' => (int) $this->faker->postcode 152 | ] 153 | ] 154 | ]; 155 | 156 | $person = new Person($personData); 157 | 158 | $serialized = json_encode($person); 159 | 160 | $this->assertJson($serialized, 'Could not serialise nested instances'); 161 | } 162 | 163 | /** 164 | * @test 165 | */ 166 | public function canResolveUsingOverloadMethodDirectly() 167 | { 168 | $person = new Person(); 169 | 170 | $data = [ 171 | 'street' => $this->faker->streetName, 172 | 'city' => [ 173 | 'name' => $this->faker->city, 174 | 'zipCode' => (int) $this->faker->postcode 175 | ] 176 | ]; 177 | 178 | $person->address = $data; 179 | 180 | $this->assertSame($data['city']['zipCode'], $person->address->city->zipCode, 181 | 'ZipCode was expected to be of a different value!'); 182 | } 183 | 184 | /** 185 | * @test 186 | * 187 | * @expectedException \Illuminate\Contracts\Container\BindingResolutionException 188 | */ 189 | public function failsPopulatingUnboundAbstractInstances() 190 | { 191 | $personData = [ 192 | 'name' => $this->faker->name, 193 | 'address' => [ 194 | 'street' => $this->faker->streetName, 195 | 'city' => [ 196 | 'name' => $this->faker->city, 197 | 'zipCode' => (int) $this->faker->postcode 198 | ] 199 | ], 200 | 201 | // Person expects a type of `NotesInterface` 202 | // which in this test has NOT been bound, thus 203 | // this should fail - Laravel's container should 204 | // make sure of that 205 | 'notes' => [ 206 | 'notes' => [ 207 | $this->faker->sentence, 208 | $this->faker->sentence, 209 | $this->faker->sentence, 210 | ] 211 | ] 212 | ]; 213 | 214 | $person = new Person($personData); 215 | } 216 | 217 | /** 218 | * @test 219 | */ 220 | public function canResolveAndPopulateBoundAbstractInstances() 221 | { 222 | 223 | // Bind the abstraction / interface 224 | $this->getContainer()->bind(NotesInterface::class, function ($app) { 225 | return new Notes(); 226 | }); 227 | 228 | $personData = [ 229 | 'name' => $this->faker->name, 230 | 'address' => [ 231 | 'street' => $this->faker->streetName, 232 | 'city' => [ 233 | 'name' => $this->faker->city, 234 | 'zipCode' => (int) $this->faker->postcode 235 | ] 236 | ], 237 | 238 | // Here, the interface is bound, thus this should 239 | // not fail 240 | 'notes' => [ 241 | 'notes' => [ 242 | $this->faker->sentence, 243 | $this->faker->sentence, 244 | $this->faker->sentence, 245 | ] 246 | ] 247 | ]; 248 | 249 | $person = new Person($personData); 250 | 251 | $arr = $personData['notes']['notes']; 252 | $actualNotes = $person->notes->getNotes(); 253 | foreach($arr as $value){ 254 | $result = in_array($value, $actualNotes); 255 | $this->assertTrue($result, 'A value was not in the notes list'); 256 | } 257 | } 258 | 259 | /** 260 | * In this test, the given `badInstance` property is of the concrete type 261 | * `BadUnpopulatableObject`, which does not inherit from `Populatable` 262 | * interface and thus we do not know how to populate it and should fail! 263 | * 264 | * @test 265 | * 266 | * @expectedException \Illuminate\Contracts\Container\BindingResolutionException 267 | */ 268 | public function failsResolvingConcreteUnpopulatableInstance() 269 | { 270 | $personData = [ 271 | 'badInstance' => [ 272 | 'foo' => 'bar' 273 | ] 274 | ]; 275 | 276 | $person = new Person($personData); 277 | } 278 | 279 | /** 280 | * In this test, we should be able to populate `Person`, with 281 | * the `BadUnpopulatableObject`, when given as a concrete instance. 282 | * 283 | *
284 | * 285 | * WARNING: You should avoid creating your DTOs without 286 | * inheritance from the `Populatable` and `Arrayable` interfaces 287 | * (minimum requirements). 288 | * 289 | * @see canPopulateWithNestedObjectInstance Similar test! 290 | * 291 | * @test 292 | */ 293 | public function canPopulateWithConcreteBadInstance() 294 | { 295 | $foo = $this->faker->word; 296 | 297 | $badInstance = new BadUnpopulatableObject(); 298 | $badInstance->setFoo($foo); 299 | 300 | $personData = [ 301 | 'badInstance' => $badInstance 302 | ]; 303 | 304 | $person = new Person($personData); 305 | 306 | $this->assertSame($foo, $person->badInstance->getFoo(), 'Foor should had been set'); 307 | } 308 | } -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 |