├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── CastsValueObjects.php └── ValueObject.php └── tests ├── CastsValueObjectTest.php ├── TestCase.php ├── bootstrap.php └── bootstrap ├── EmailValueObject.php └── UserModel.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - nightly 6 | - hhvm 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --prefer-source --no-interaction --dev 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 S. Zain Mehdi 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Laravel Value Objects 2 | [![Build Status](https://travis-ci.org/redcrystalcode/laravel-value-objects.svg?branch=master)](https://travis-ci.org/redcrystal/cast) 3 | [![Latest Stable Version](https://poser.pugx.org/redcrystal/cast/version.png)](https://packagist.org/packages/redcrystal/cast) 4 | [![Total Downloads](https://poser.pugx.org/redcrystal/cast/d/total.png)](https://packagist.org/packages/redcrystal/cast) 5 | 6 | 7 | Cast your Eloquent model attributes to value objects with ease! 8 | 9 | ### Requirements 10 | 11 | This package requires PHP >= 5.4. Using the latest version of PHP is highly recommended. Laravel 4.x and 5.x are supported. 12 | 13 | > **Note:** Running tests for this package requires PHP >=5.6. 14 | 15 | ### Install 16 | 17 | Require this package with composer using the following command: 18 | 19 | ```bash 20 | composer require redcrystal/cast 21 | ``` 22 | 23 | ### Set Up 24 | 25 | This package lets you easily cast your model attributes to Value Objects that implement our `RedCrystal\Cast\ValueObject` interface. A simple example is provided below. 26 | 27 | ```php 28 | value = $value; 40 | } 41 | 42 | public function toScalar() 43 | { 44 | return $this->value; 45 | } 46 | 47 | public function __toString() { 48 | return $this->toScalar(); 49 | } 50 | } 51 | ``` 52 | 53 | Set up your model by using the included `Trait` and adding a tiny bit of configuration. 54 | 55 | ```php 56 | name of the value object class 70 | 'email' => Email::class 71 | ]; 72 | 73 | // ... 74 | } 75 | ``` 76 | 77 | ### Usage 78 | 79 | When accessing attributes of your model normally, any attribute you've set up for casting will be returned as an instance of the Value Object. 80 | 81 | ```php 82 | $user = User::find($id); 83 | 84 | $user->email; // returns instance of App\ValueObjects\Email 85 | $user->email->toScalar(); // "someone@example.com" 86 | (string) $user->email; // "someone@example.com" 87 | ``` 88 | 89 | You can set an attribute set up for casting with either a scalar (native) value, or an instance of the Value Object. 90 | 91 | ```php 92 | $user = new User(); 93 | 94 | $user->email = "someone@example.com"; 95 | $user->email = new Email("someone@example.com"); 96 | ``` 97 | 98 | ### License 99 | This package is open-source software licensed under the [MIT license](http://opensource.org/licenses/MIT). 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redcrystal/cast", 3 | "description": "Laravel Value Objects: Cast your Eloquent model attributes to value objects with ease!", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Zain Mehdi", 8 | "email": "szainmehdi@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "RedCrystal\\Cast\\": "src/" 14 | }, 15 | "classmap": [ 16 | "tests/TestCase.php" 17 | ] 18 | }, 19 | "require": { 20 | "php": ">=5.4", 21 | "laravel/framework": ">=4.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^5.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ../src/ 15 | 16 | 17 | 18 | 19 | ./tests/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CastsValueObjects.php: -------------------------------------------------------------------------------- 1 | objects) && is_array($this->objects) && isset($this->objects[$key])) { 22 | 23 | // Allow other mutators and such to do their work first. 24 | $value = parent::getAttribute($key); 25 | 26 | // Don't cast empty $value. 27 | if ($value === null || $value === '') { 28 | return null; 29 | } 30 | 31 | // Cache the instantiated value for future access. 32 | // This allows tests such as ($model->casted === $model->casted) to be true. 33 | if (!$this->isValueObjectCached($key)) { 34 | $this->cacheValueObject( 35 | $key, 36 | $this->createValueObject($key, $value) 37 | ); 38 | } 39 | 40 | return $this->getCachedValueObject($key); 41 | } 42 | 43 | return parent::getAttribute($key); 44 | } 45 | 46 | /** 47 | * @param string $key 48 | * @param mixed $value 49 | */ 50 | public function setAttribute($key, $value) 51 | { 52 | if ($value instanceof ValueObject) { 53 | // The value provided is a value object. We'll need to cast it to a scalar 54 | // and then let it be set into Eloquent's attributes array. 55 | $scalar = $value->toScalar(); 56 | parent::setAttribute($key, $scalar); 57 | 58 | // Housekeeping. 59 | if ($this->attributes[$key] === $scalar) { 60 | // If the value wasn't modified during the set process 61 | // store the original ValueObject in our cache. 62 | $this->cacheValueObject($key, $value); 63 | } else { 64 | // Otherwise, we'll invalidate the cache for this key and defer 65 | // to the get action for re-instantiating the ValueObject. 66 | $this->invalidateValueObjectCache($key); 67 | } 68 | } elseif ($this->isValueObjectCached($key)) { 69 | // This means that an attribute that has been cached to a ValueObject is being 70 | // set directly as a scalar. We'll invalidate the cached ValueObject and defer 71 | // to the get action for re-instantiating the ValueObject. 72 | $this->invalidateValueObjectCache($key); 73 | parent::setAttribute($key, $value); 74 | } else { 75 | // Standard value given. No need to do anything special. 76 | parent::setAttribute($key, $value); 77 | } 78 | } 79 | 80 | /** 81 | * @param string $key 82 | * 83 | * @return mixed 84 | */ 85 | private function createValueObject($key, $value) 86 | { 87 | $class = $this->objects[$key]; 88 | 89 | return new $class($value); 90 | } 91 | 92 | /** 93 | * @param string $key 94 | * @param ValueObject $object 95 | */ 96 | private function cacheValueObject($key, ValueObject $object) 97 | { 98 | $this->cachedObjects[$key] = $object; 99 | } 100 | 101 | /** 102 | * @param string $key 103 | */ 104 | private function invalidateValueObjectCache($key) 105 | { 106 | unset($this->cachedObjects[$key]); 107 | } 108 | 109 | /** 110 | * @param string $key 111 | * 112 | * @return bool 113 | */ 114 | private function isValueObjectCached($key) 115 | { 116 | return isset($this->cachedObjects[$key]); 117 | } 118 | 119 | /** 120 | * @param string $key 121 | * 122 | * @return ValueObject 123 | */ 124 | private function getCachedValueObject($key) 125 | { 126 | return $this->cachedObjects[$key]; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/ValueObject.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(UserModel::class, $user); 9 | } 10 | 11 | public function testInstantiateValueObject() 12 | { 13 | $email = new EmailValueObject('user@example.com'); 14 | $this->assertInstanceOf(EmailValueObject::class, $email); 15 | } 16 | 17 | public function testAttributeInObjectsHashIsCastToValueObject() 18 | { 19 | $user = new UserModel(); 20 | $user->setInternalAttributes(['email' => $email = 'user@example.com']); 21 | 22 | $this->assertInstanceOf(EmailValueObject::class, $user->email); 23 | $this->assertEquals($email, $user->email->toScalar()); 24 | } 25 | 26 | public function testAttributeInObjectsHashCanBeSetWithScalarValue() 27 | { 28 | $user = new UserModel(); 29 | 30 | $user->email = $email = 'user@example.com'; 31 | 32 | $this->assertInstanceOf(EmailValueObject::class, $user->email); 33 | $this->assertEquals($email, $user->email->toScalar()); 34 | } 35 | 36 | public function testAttributeInObjectsHashCanBeSetWithValueObject() 37 | { 38 | $user = new UserModel(); 39 | 40 | $user->email = $email = new EmailValueObject('user@example.com'); 41 | $this->assertInstanceOf(EmailValueObject::class, $user->email); 42 | $this->assertEquals($email->toScalar(), $user->email->toScalar()); 43 | } 44 | 45 | public function testCastedValueObjectRemainsTheSameInstance() 46 | { 47 | $user = new UserModel(); 48 | 49 | $user->email = $instance1 = new EmailValueObject('user@example.com'); 50 | $instance2 = $user->email; 51 | 52 | $this->assertTrue($instance1 === $instance2); 53 | 54 | $instance3 = $user->email; 55 | $this->assertTrue($instance1 === $instance3); 56 | $this->assertTrue($instance2 === $instance3); 57 | } 58 | 59 | public function testAttributeNotInObjectsHashRemainsUnaffected() 60 | { 61 | $user = new UserModel(); 62 | $user->name = $name = 'John Doe'; 63 | 64 | $this->assertTrue(is_string($user->name)); 65 | $this->assertEquals($name, $user->name); 66 | } 67 | 68 | public function testCastableAttributeWithSetMutator() 69 | { 70 | $user = new UserModel(); 71 | $user->uppercaseEmail = 'user@example.com'; 72 | 73 | $this->assertInstanceOf(EmailValueObject::class, $user->uppercaseEmail); 74 | $this->assertEquals($user->getInternalAttributes()['uppercaseEmail'], $user->uppercaseEmail->toScalar()); 75 | } 76 | 77 | public function testCastableAttributeWithGetMutator() 78 | { 79 | $user = new UserModel(); 80 | $user->mutatedEmail = $original = 'user@example.com'; 81 | 82 | $this->assertEquals($user->getInternalAttributes()['mutatedEmail'], $original); 83 | $this->assertInstanceOf(EmailValueObject::class, $user->mutatedEmail); 84 | $this->assertEquals($user->getMutatedEmailAttribute($original), $user->mutatedEmail->toScalar()); 85 | } 86 | 87 | public function testValueObjectCacheIsInvalidatedWhenSettingScalar() 88 | { 89 | $user = new UserModel(); 90 | 91 | $user->email = $email = 'user@example.com'; 92 | 93 | $this->assertInstanceOf(EmailValueObject::class, $user->email); 94 | $this->assertEquals($email, $user->email->toScalar()); 95 | 96 | $user->email = $email = 'someone@example.com'; 97 | 98 | $this->assertInstanceOf(EmailValueObject::class, $user->email); 99 | $this->assertEquals($email, $user->email->toScalar()); 100 | } 101 | 102 | public function testModelToArrayWithValueObjects() 103 | { 104 | $user = new UserModel(); 105 | 106 | $user->email = $email = 'user@example.com'; 107 | 108 | $array = $user->toArray(); 109 | 110 | $this->assertArrayHasKey('email', $array); 111 | $this->assertTrue(is_string($array['email'])); 112 | $this->assertFalse($array['email'] instanceof EmailValueObject); 113 | } 114 | 115 | public function testModelToJsonWithValueObjects() 116 | { 117 | $user = new UserModel(); 118 | 119 | $user->email = $email = 'user@example.com'; 120 | 121 | $json = $user->toJson(); 122 | 123 | $this->assertJson($json); 124 | $this->assertJsonStringEqualsJsonString(json_encode(['email' => $email]), $json); 125 | } 126 | 127 | public function testNullValuesDontGetCast() 128 | { 129 | $user = new UserModel(); 130 | 131 | $this->assertNull($user->email); 132 | } 133 | 134 | public function testEmptyStringValuesDontGetCast() 135 | { 136 | $user = new UserModel(); 137 | 138 | $user->setInternalAttributes(['email' => '']); 139 | 140 | $this->assertNull($user->email); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | value = $value; 12 | } 13 | 14 | public function toScalar() 15 | { 16 | return $this->value; 17 | } 18 | 19 | public function __toString() { 20 | return $this->toScalar(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/bootstrap/UserModel.php: -------------------------------------------------------------------------------- 1 | EmailValueObject::class, 11 | 'uppercaseEmail' => EmailValueObject::class, 12 | 'mutatedEmail' => EmailValueObject::class 13 | ]; 14 | 15 | // Laravel/Eloquent magic mutator 16 | public function setUppercaseEmailAttribute($value) 17 | { 18 | $this->attributes['uppercaseEmail'] = strtoupper($value); 19 | } 20 | 21 | // Laravel/Eloquent magic mutator 22 | public function getMutatedEmailAttribute($value) 23 | { 24 | return str_replace('example.com', 'redcode.io', $value); 25 | } 26 | 27 | // In order to assist with testing, I've added a few methods to expose internals. 28 | public function getInternalAttributes() 29 | { 30 | return $this->attributes; 31 | } 32 | 33 | public function setInternalAttributes(array $attributes) 34 | { 35 | $this->attributes = $attributes; 36 | } 37 | } 38 | --------------------------------------------------------------------------------