Skip to content

Commit 4d177ae

Browse files
committed
Initial commit.
0 parents  commit 4d177ae

11 files changed

+395
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/vendor/
2+
composer.lock
3+
.idea/

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

composer.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "redcrystal/cast",
3+
"description": "Cast your model attributes to value objects!",
4+
"license": "MIT",
5+
"authors": [
6+
{
7+
"name": "Zain Mehdi",
8+
"email": "[email protected]"
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+
}

phpunit.xml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit backupGlobals="false"
3+
backupStaticAttributes="false"
4+
bootstrap="tests/bootstrap.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false"
11+
syntaxCheck="false">
12+
<filter>
13+
<whitelist>
14+
<directory suffix=".php">../src/</directory>
15+
</whitelist>
16+
</filter>
17+
<testsuites>
18+
<testsuite name="Casts Test Suite">
19+
<directory suffix=".php">./tests/</directory>
20+
</testsuite>
21+
</testsuites>
22+
</phpunit>

src/CastsValueObjects.php

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
namespace RedCrystal\Cast;
3+
4+
/**
5+
* Class CastsValueObjects
6+
*
7+
* @package RedCrystal\Cast
8+
*/
9+
trait CastsValueObjects
10+
{
11+
/** @var array */
12+
protected $cachedObjects = [];
13+
14+
/**
15+
* @param $key
16+
*
17+
* @return mixed
18+
*/
19+
public function getAttribute($key)
20+
{
21+
if (isset($this->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+
// Cache the instantiated value for future access.
27+
// This allows tests such as ($model->casted === $model->casted) to be true.
28+
if (!$this->isValueObjectCached($key)) {
29+
$this->cacheValueObject(
30+
$key,
31+
$this->createValueObject($key, $value)
32+
);
33+
}
34+
35+
return $this->getCachedValueObject($key);
36+
}
37+
38+
return parent::getAttribute($key);
39+
}
40+
41+
/**
42+
* @param $key
43+
* @param $value
44+
*/
45+
public function setAttribute($key, $value)
46+
{
47+
if ($value instanceof ValueObject) {
48+
// The value provided is a value object. We'll need to cast it to a scalar
49+
// and then let it be set into Eloquent's attributes array.
50+
$scalar = $value->toScalar();
51+
parent::setAttribute($key, $scalar);
52+
53+
// Housekeeping.
54+
if ($this->attributes[$key] === $scalar) {
55+
// If the value wasn't modified during the set process
56+
// store the original ValueObject in our cache.
57+
$this->cacheValueObject($key, $value);
58+
} else {
59+
// Otherwise, we'll invalidate the cache for this key and defer
60+
// to the get action for re-instantiating the ValueObject.
61+
$this->invalidateValueObjectCache($key);
62+
}
63+
} elseif ($this->isValueObjectCached($key)) {
64+
// This means that an attribute that has been cached to a ValueObject is being
65+
// set directly as a scalar. We'll invalidate the cached ValueObject and defer
66+
// to the get action for re-instantiating the ValueObject.
67+
$this->invalidateValueObjectCache($key);
68+
parent::setAttribute($key, $value);
69+
} else {
70+
// Standard value given. No need to do anything special.
71+
parent::setAttribute($key, $value);
72+
}
73+
}
74+
75+
/**
76+
* @param $key
77+
*
78+
* @return mixed
79+
*/
80+
private function createValueObject($key, $value)
81+
{
82+
$class = $this->objects[$key];
83+
84+
return new $class($value);
85+
}
86+
87+
/**
88+
* @param $key
89+
* @param ValueObject $object
90+
*/
91+
private function cacheValueObject($key, ValueObject $object)
92+
{
93+
$this->cachedObjects[$key] = $object;
94+
}
95+
96+
/**
97+
* @param $key
98+
*/
99+
private function invalidateValueObjectCache($key)
100+
{
101+
unset($this->cachedObjects[$key]);
102+
}
103+
104+
/**
105+
* @param $key
106+
*
107+
* @return bool
108+
*/
109+
private function isValueObjectCached($key)
110+
{
111+
return isset($this->cachedObjects[$key]);
112+
}
113+
114+
/**
115+
* @param $key
116+
*
117+
* @return ValueObject
118+
*/
119+
private function getCachedValueObject($key)
120+
{
121+
return $this->cachedObjects[$key];
122+
}
123+
124+
}

src/ValueObject.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
namespace RedCrystal\Cast;
3+
4+
/**
5+
* Interface ValueObject
6+
*
7+
* @package RedCrystal\Cast
8+
*/
9+
interface ValueObject
10+
{
11+
/**
12+
* @param $value
13+
*/
14+
public function __construct($value);
15+
16+
/**
17+
* @return mixed
18+
*/
19+
public function toScalar();
20+
21+
/**
22+
* @return string
23+
*/
24+
public function __toString();
25+
26+
}

tests/CastsValueObjectTest.php

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
class CastsValueObjectTest extends TestCase
4+
{
5+
public function testInstantiateModel()
6+
{
7+
$user = new UserModel();
8+
$this->assertInstanceOf(UserModel::class, $user);
9+
}
10+
11+
public function testInstantiateValueObject()
12+
{
13+
$email = new EmailValueObject('[email protected]');
14+
$this->assertInstanceOf(EmailValueObject::class, $email);
15+
}
16+
17+
public function testAttributeInObjectsHashIsCastToValueObject()
18+
{
19+
$user = new UserModel();
20+
$user->setInternalAttributes(['email' => $email = '[email protected]']);
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 = '[email protected]';
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('[email protected]');
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('[email protected]');
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 = '[email protected]';
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 = '[email protected]';
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 = '[email protected]';
92+
93+
$this->assertInstanceOf(EmailValueObject::class, $user->email);
94+
$this->assertEquals($email, $user->email->toScalar());
95+
96+
$user->email = $email = '[email protected]';
97+
98+
$this->assertInstanceOf(EmailValueObject::class, $user->email);
99+
$this->assertEquals($email, $user->email->toScalar());
100+
}
101+
}

tests/TestCase.php

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
abstract class TestCase extends PHPUnit_Framework_TestCase
4+
{
5+
6+
}

tests/bootstrap.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
require __DIR__ . '/../vendor/autoload.php';
4+
5+
// Require our sample classes for the tests.
6+
require 'bootstrap/UserModel.php';
7+
require 'bootstrap/EmailValueObject.php';

tests/bootstrap/EmailValueObject.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use RedCrystal\Cast\ValueObject;
4+
5+
class EmailValueObject implements ValueObject
6+
{
7+
protected $value;
8+
9+
public function __construct($value)
10+
{
11+
$this->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+
}

0 commit comments

Comments
 (0)