diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c69172 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# Data models + +Data models are the wrapper classes to the JSON strings or php arrays (markup languages in the future). Models simplifies the manipulation and processing workflow for the JSON or array objects. + +## Pros + +- Avoid undefined index by design +- Dynamic access to the model properties so no need of mapping the class properties with JSON attributes +- IDE auto-completion using `@property` docblock +- Set has many and has one relationships between models +- Ability to assign default values for the attributes so the undefined attributes can be handled reliably +- Ability to add logic into the JSON data in the model +- Cast values to known types such as integer, string, float, boolean +- Cast values to Carbon object to work on date attributes easily +- Ability to implement custom cast types +- Manipulate the object and make it array or serialize to JSON back + +## Usage + +Imagine you have a JSON data for a blog post looks like this + +``` +$data = '{ + "id": 1, + "author": "Can Gelis", + "created_at": "2019-05-11 22:00:00", + "comments": [ + { + "id": 1, + "text": "Hello World!" + }, + { + "id": 2, + "text": "What a wonderful world!" + } + ], + "settings": {"comments_enable": 1} +}'; +``` + +You can create the models looks like this + +```php + +use CanGelis\DataModels\JsonModel; +use CanGelis\DataModels\Cast\BooleanCast; +use CanGelis\DataModels\Cast\DateTimeCast; + +/** +* Define docblock for ide auto-completion +* +* @property bool $comments_enable +*/ +class Settings extends JsonModel { + + protected $casts = ['comments_enable' => BooleanCast::class]; + + protected $defaults = ['comments_enable' => false]; + +} + +/** +* Define docblock for ide auto-completion +* +* @property integer $id +* @property string $text +*/ +class Comment extends JsonModel {} + +/** +* Define docblock for ide auto-completion +* +* @property integer $id +* @property author $text +* @property Carbon\Carbon $created_at +* @property Settings $settings +* @property CanGelis\DataModels\DataCollection $comments +*/ +class Post extends JsonModel { + + protected $defaults = ['text' => 'No Text']; + + protected $casts = ['created_at' => DateTimeCast::class]; + + protected $hasMany = ['comments' => Comment::class]; + + protected $hasOne = ['settings' => Settings::class]; + +} + +``` + +Use the models + +```php + +$post = Post::fromString($data); // initialize from JSON String +$post = new Post(json_decode($data, true)); // or use arrays + +$post->text // "No Text" in $defaults +$post->foo // returns null which doesn't have default value +$post->created_at // get Carbon object +$post->created_at->addDay(1) // Go to tomorrow +$post->created_at = Carbon::now() // update the creation time + +$post->settings->comments_enable // returns true +$post->settings->comments_enable = false // manipulate the object +$post->settings->comments_enable // returns false +$post->settings->editable = false // introduce a new attribute + +$post->comments->first() // returns the first comment +$post->comments[1] // get the second comment +foreach ($post->comments as $comment) {} // iterate on comments +$post->comments->add(['id' => 3, 'text' => 'Not too bad']) // add to the collection + +$post->toArray() // see as array +$post->jsonSerialize() // serialize to json + +/* +{"id":1,"author":"Can Gelis","created_at":"2019-11-14 16:09:32","comments":[{"id":1,"text":"Hello World!"},{"id":2,"text":"What a wonderful world!"},{"id":3,"text":"Not too bad"}],"settings":{"comments_enable":false,"editable":false}} +*/ + +``` + +## Custom Casts + +If you prefer to implement more complex value casting logic, data models allow you to implement your custom ones. + +Imagine you use Laravel Eloquent and want to cast an in a JSON attribute. + +```php + +// data = {"id": 1, "user": 1} + +class EloquentUserCast extends AbstractCast { + + /** + * The value is casted when it is accessed + * So this is a good place to convert the value in the + * JSON into what we'd like to see + * + * @param mixed $value + * + * @return mixed + */ + public function cast($value) + { + if (!$value instanceof User) { + return User::find($value); + } + return $value; + } + + /** + * This method is called when the object is serialized back to + * array or JSON + * So this is good place to make the values + * json compatible such as integer, string or bool + * + * @param mixed $value + * + * @return mixed + */ + public function uncast($value) + { + if ($value instanceof User) { + return $value->id; + } + return $value; + } +} + +class Post { + + protected $casts = ['user' => EloquentUserCast::class]; + +} + +$post->user = User::find(2); // set the Eloquent model directly +$post->user = 2; // set only the id instead +$post->user // returns instance of User +$post->toArray() + +['id' => 1, 'user' => 2] + +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fc626a0 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "cangelis/data-models", + "description": "Yet another HTML to PDF Converter based on wkhtmltopdf", + "keywords": ["json", "mapper", "data", "dto", "xml"], + "license": "MIT", + "authors": [ + { + "name": "Can Geliş", + "email": "geliscan@gmail.com" + } + ], + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6" + }, + "suggest": { + "nesbot/carbon": "Allows you to cast date attributes", + "ext-json": "*" + }, + "autoload": { + "psr-0": { + "CanGelis\\": "src/" + } + }, + "minimum-stability": "dev" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..dffa2d4 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + ./tests + + + diff --git a/src/CanGelis/DataModels/Cast/AbstractCast.php b/src/CanGelis/DataModels/Cast/AbstractCast.php new file mode 100644 index 0000000..f1ed4b7 --- /dev/null +++ b/src/CanGelis/DataModels/Cast/AbstractCast.php @@ -0,0 +1,31 @@ +toDateString(); + } + return $value; + } +} diff --git a/src/CanGelis/DataModels/Cast/DateTimeCast.php b/src/CanGelis/DataModels/Cast/DateTimeCast.php new file mode 100644 index 0000000..47925a3 --- /dev/null +++ b/src/CanGelis/DataModels/Cast/DateTimeCast.php @@ -0,0 +1,27 @@ +toDateTimeString(); + } + return $value; + } +} diff --git a/src/CanGelis/DataModels/Cast/FloatCast.php b/src/CanGelis/DataModels/Cast/FloatCast.php new file mode 100644 index 0000000..9b06142 --- /dev/null +++ b/src/CanGelis/DataModels/Cast/FloatCast.php @@ -0,0 +1,14 @@ +toIso8601String(); + } + return $value; + } +} diff --git a/src/CanGelis/DataModels/Cast/StringCast.php b/src/CanGelis/DataModels/Cast/StringCast.php new file mode 100644 index 0000000..6c382b3 --- /dev/null +++ b/src/CanGelis/DataModels/Cast/StringCast.php @@ -0,0 +1,14 @@ +items = $items; + } + + /** + * @inheritDoc + */ + public function offsetExists($offset) + { + return isset($this->items[$offset]); + } + + /** + * @inheritDoc + */ + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + /** + * @inheritDoc + */ + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** + * @inheritDoc + */ + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + /** + * @inheritDoc + */ + public function toJson() + { + return json_encode($this->items); + } + + /** + * @inheritDoc + */ + public function toArray() + { + return array_map(function ($item) { + if ($item instanceof DataModel) { + return $item->toArray(); + } + return $item; + }, $this->items); + } + + /** + * Add an item to the collection. + * + * @param \CanGelis\DataModels\DataModel $item + * + * @return $this + */ + public function add(DataModel $item) + { + $this->items[] = $item; + + return $this; + } + + /** + * @inheritDoc + */ + public function getIterator() + { + return new \ArrayIterator($this->items); + } + + /** + * @inheritDoc + */ + public function count() + { + return count($this->items); + } + + /** + * Get the first item + * + * @param callable|null $callback + * @param mixed $default + * + * @return mixed + */ + public function first(callable $callback = null, $default = null) + { + if (is_null($callback)) { + $callback = function ($item) { + return true; + }; + } + + foreach ($this->items as $item) { + if ($callback($item)) { + return $item; + } + } + + return $default; + } + + /** + * @inheritDoc + */ + public function filter(callable $callback) + { + return array_filter($this->items, $callback); + } +} diff --git a/src/CanGelis/DataModels/DataModel.php b/src/CanGelis/DataModels/DataModel.php new file mode 100644 index 0000000..010c3ce --- /dev/null +++ b/src/CanGelis/DataModels/DataModel.php @@ -0,0 +1,337 @@ +data = $data; + } + + /** + * Make an array + * + * @return array + */ + public function toArray() + { + $data = $this->data; + // apply modified relationships + foreach ($this->relations as $relationAttribute => $relation) { + list($relationType, $attribute) = explode("-", $relationAttribute); + $data[$attribute] = $relation->toArray(); + } + return $data; + } + + /** + * Accessor for the json object + * + * @param string $attribute + * + * @return mixed + */ + public function __get($attribute) + { + // resolve has many relationship + if (array_key_exists($attribute, $this->hasMany)) { + return $this->getHasManyValue($attribute); + } + + // resolve has one relationship + if (array_key_exists($attribute, $this->hasOne)) { + return $this->getHasOneValue($attribute); + } + + // return if it was accessed before + if (array_key_exists($attribute, $this->attributeValues)) { + return $this->attributeValues[$attribute]; + } + + if (array_key_exists($attribute, $this->data)) { + return $this->attributeValues[$attribute] = $this->castValue($attribute, $this->data[$attribute]); + } + + if (array_key_exists($attribute, $this->getDefaults())) { + return $this->attributeValues[$attribute] = $this->castValue($attribute, $this->getDefaults()[$attribute]); + } + + return $this->attributeValues[$attribute] = null; + } + + /** + * Set the value + * + * @param string $attribute + * @param mixed $value + * + * @throws \InvalidArgumentException + */ + public function __set($attribute, $value) + { + if (array_key_exists($attribute, $this->hasOne)) { + $this->setHasOneValue($attribute, $value); + } elseif (array_key_exists($attribute, $this->hasMany)) { + $this->setHasManyValue($attribute, $value); + } else { + $this->data[$attribute] = $this->uncastValue($attribute, $value); + unset($this->attributeValues[$attribute]); + } + } + + /** + * @inheritDoc + */ + public function __isset($name) + { + return isset($this->data[$name]); + } + + /** + * @inheritDoc + */ + public function __unset($name) + { + unset($this->data[$name]); + } + + /** + * Get has many relationship value + * + * @param mixed $attribute + * + * @return \CanGelis\DataModels\DataCollection + */ + protected function getHasManyValue($attribute) + { + if (isset($this->relations['hasMany-' . $attribute])) { + return $this->relations['hasMany-' . $attribute]; + } + + $items = []; + if (array_key_exists($attribute, $this->data) && is_array($this->data[$attribute])) { + $items = $this->data[$attribute]; + } + + return $this->relations['hasMany-' . $attribute] = $this->makeCollection( + array_map(function ($item) use ($attribute) { + return $this->getItemAsObject($item, $this->hasMany[$attribute]); + }, $items) + ); + } + + /** + * Make the value array if it is an datamodel already + * + * @param \CanGelis\DataModels\DataModel|array $item + * + * @return array + */ + protected function getItemAsArray($item) + { + if ($item instanceof DataModel) { + return $item->toArray(); + } + + if (is_array($item)) { + return $item; + } + + throw new \InvalidArgumentException('Expected array or DataModel but ' . gettype($item) . ' given'); + } + + /** + * Make item an data model + * + * @param array|\CanGelis\DataModels\DataModel $item + * @param string $class + * + * @return \CanGelis\DataModels\DataModel + */ + protected function getItemAsObject($item, $class) + { + if (is_array($item)) { + return new $class($item); + } + + if (get_class($item) == $class) { + return $item; + } + + throw new \InvalidArgumentException('Expected array or ' . $class . ' but ' . gettype($item) . ' given'); + } + + /** + * Get the has one relationship value + * + * @param mixed $attribute + * + * @return \CanGelis\DataModels\DataModel|null + */ + protected function getHasOneValue($attribute) + { + if (isset($this->relations['hasOne-' . $attribute])) { + return $this->relations['hasOne-' . $attribute]; + } + + if (is_array($this->data[$attribute])) { + return $this->relations['hasOne-' . $attribute] = new $this->hasOne[$attribute]($this->data[$attribute]); + } + + return $this->relations['hasOne-' . $attribute] = null; + } + + /** + * Set has one value + * + * @param string $attribute + * @param array|\CanGelis\DataModels\DataModel $value + */ + protected function setHasOneValue($attribute, $value) + { + $this->data[$attribute] = $this->getItemAsArray($value); + unset($this->relations['hasOne-' . $attribute]); + } + + /** + * Set has many value + * + * @param string $attribute + * @param \CanGelis\DataModels\DataCollection $value + */ + protected function setHasManyValue($attribute, $value) + { + if (is_array($value)) { + $this->data[$attribute] = array_map(function ($item) { + return $this->getItemAsArray($item); + }, $value); + } elseif ($value instanceof DataCollection) { + $this->data[$attribute] = $value->toArray(); + } else { + throw new \InvalidArgumentException('Expected array or DataCollection but ' . gettype($value) . ' given'); + } + + unset($this->relations['hasMany-' . $attribute]); + } + + /** + * Default values for the attributes that doesn't exist + * in the data, don't hesitate to override this if you have + * more complex defaults logic + * + * @return array + */ + protected function getDefaults() + { + return $this->defaults; + } + + /** + * Cast an attribute value + * + * @param string $attribute + * @param string $value + * + * @return mixed + */ + protected function castValue($attribute, $value) + { + if (!array_key_exists($attribute, $this->casts)) { + return $value; + } + + /** + * @var \CanGelis\DataModels\Cast\AbstractCast $caster + */ + $caster = new $this->casts[$attribute](); + + return $caster->cast($value); + } + + /** + * Revert casted value back to the serialiazable form + * + * @param string $attribute + * @param mixed $value + * + * @return mixed + */ + protected function uncastValue($attribute, $value) + { + if (!array_key_exists($attribute, $this->casts)) { + return $value; + } + + /** + * @var \CanGelis\DataModels\Cast\AbstractCast $caster + */ + $caster = new $this->casts[$attribute](); + + return $caster->uncast($value); + } + + /** + * Make a new collection + * + * @param array $items + * + * @return \CanGelis\DataModels\DataCollection + */ + protected function makeCollection($items) + { + return new DataCollection($items); + } +} diff --git a/src/CanGelis/DataModels/JsonModel.php b/src/CanGelis/DataModels/JsonModel.php new file mode 100644 index 0000000..3eb5315 --- /dev/null +++ b/src/CanGelis/DataModels/JsonModel.php @@ -0,0 +1,34 @@ +toArray()); + } + + /** + * @inheritDoc + */ + public function __toString() + { + return $this->jsonSerialize(); + } +} diff --git a/tests/CastTest.php b/tests/CastTest.php new file mode 100644 index 0000000..cd17f1c --- /dev/null +++ b/tests/CastTest.php @@ -0,0 +1,103 @@ + FloatCast::class, + 'age' => IntegerCast::class, + 'has_license' => BooleanCast::class, + 'license_number' => StringCast::class + ]; + +} + +class CastTest extends TestCase +{ + public function testBoolean() + { + $player = new Player(['has_license' => 'false']); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertFalse($player->has_license); + $player = new Player(['has_license' => null]); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertFalse($player->has_license); + $player = new Player(['has_license' => false]); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertFalse($player->has_license); + $player = new Player(['has_license' => 0]); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertFalse($player->has_license); + $player = new Player(['has_license' => 'true']); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertTrue($player->has_license); + $player = new Player(['has_license' => true]); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertTrue($player->has_license); + $player = new Player(['has_license' => 1]); + $this->assertEquals('boolean', gettype($player->has_license)); + $this->assertTrue($player->has_license); + } + + public function testInteger() + { + $player = new Player(['age' => 10]); + $this->assertEquals('integer', gettype($player->age)); + $this->assertEquals(10, $player->age); + $player = new Player(['age' => '10']); + $this->assertEquals('integer', gettype($player->age)); + $this->assertEquals(10, $player->age); + $player = new Player(['age' => '10.0']); + $this->assertEquals('integer', gettype($player->age)); + $this->assertEquals(10, $player->age); + $player = new Player(['age' => 10.0]); + $this->assertEquals('integer', gettype($player->age)); + $this->assertEquals(10, $player->age); + } + + public function testString() + { + $player = new Player(['license_number' => 1234]); + $this->assertEquals('string', gettype($player->license_number)); + $this->assertEquals('1234', $player->license_number); + $player = new Player(['license_number' => '1234']); + $this->assertEquals('string', gettype($player->license_number)); + $this->assertEquals('1234', $player->license_number); + $player = new Player(['license_number' => null]); + $this->assertEquals('string', gettype($player->license_number)); + $this->assertEquals('', $player->license_number); + } + + public function testFloat() + { + $player = new Player(['rate' => 10]); + $this->assertEquals('double', gettype($player->rate)); + $this->assertEquals(10.0, $player->rate); + $player = new Player(['rate' => '10']); + $this->assertEquals('double', gettype($player->rate)); + $this->assertEquals(10.0, $player->rate); + $player = new Player(['rate' => '10.1']); + $this->assertEquals('double', gettype($player->rate)); + $this->assertEquals(10.1, $player->rate); + $player = new Player(['rate' => 10.1]); + $this->assertEquals('double', gettype($player->rate)); + $this->assertEquals(10.1, $player->rate); + $player = new Player(['rate' => null]); + $this->assertEquals('double', gettype($player->rate)); + $this->assertEquals(0.0, $player->rate); + } +} diff --git a/tests/DefaultValueTest.php b/tests/DefaultValueTest.php new file mode 100644 index 0000000..b19d340 --- /dev/null +++ b/tests/DefaultValueTest.php @@ -0,0 +1,58 @@ + FloatCast::class + ]; + + protected $defaults = [ + 'author' => 'Can Gelis', + 'rate' => '0.0' + ]; +} + +class DefaultValueTest extends TestCase +{ + public function testDefaultValueIsReturnedWhenItDoesntExist() + { + $comment = new Comment([]); + $this->assertEquals('Can Gelis', $comment->author); + } + + public function testDefaultValueIsNotReturnedWhenTheValueExists() + { + $comment = new Comment(['author' => 'Foo Bar']); + $this->assertEquals('Foo Bar', $comment->author); + } + + public function testDefaultValueIsNotReturnedWhenTheValuesIsNull() + { + $comment = new Comment(['author' => null]); + $this->assertNull($comment->author); + } + + public function testReturnsNullWhenItIsNotDefault() + { + $comment = new Comment([]); + $this->assertNull($comment->text); + } + + public function testDefaultIsCasted() + { + $comment = new Comment([]); + $this->assertEquals(0.0, $comment->rate); + $this->assertEquals('double', gettype($comment->rate)); + } +} diff --git a/tests/HasManyTest.php b/tests/HasManyTest.php new file mode 100644 index 0000000..0f9954f --- /dev/null +++ b/tests/HasManyTest.php @@ -0,0 +1,116 @@ + Post::class + ]; + +} + +class HasManyTest extends TestCase { + + public function testReturnCollectionWhenDataIsArray() + { + $user = new User(['posts' => [['foo' => 'bar'], ['foo' => 'baz']]]); + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertInstanceOf(Post::class, $user->posts->first()); + $this->assertEquals(2, $user->posts->count()); + } + + public function testReturnEmptyCollectionWhenAttributeDoesNotExist() + { + $user = new User([]); + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals(0, $user->posts->count()); + } + + public function testReturnEmptyCollectionWhenAttributeIsNotAnArray() + { + $user = new User(['posts' => null]); + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals(0, $user->posts->count()); + } + + public function testArrayValuesIsSetAsExceptedWhenItIsArrayOfArray() + { + $user = new User([]); + $user->posts = [['foo' => 'bar']]; + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals('bar', $user->posts->first()->foo); + $this->assertEquals(1, $user->posts->count()); + } + + public function testModelValuesAreSetAsExpectedWhenItIsArrayOfObjects() + { + $user = new User([]); + $user->posts = [new Post(['foo' => 'bar'])]; + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals('bar', $user->posts->first()->foo); + $this->assertEquals(1, $user->posts->count()); + $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); + } + + public function testModelValuesAreSetAsExpectedWhenItIsArrayOfMixedTypes() + { + $user = new User([]); + $user->posts = [new Post(['foo' => 'bar']), ['foo' => 'baz']]; + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals('bar', $user->posts[0]->foo); + $this->assertEquals('baz', $user->posts[1]->foo); + $this->assertEquals(2, $user->posts->count()); + $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); + $this->assertEquals(['foo' => 'baz'], $user->toArray()['posts'][1]); + } + + public function testModelValuesAreSetAsExpectedWhenValuesAreProvidedAsCollection() + { + $user = new User([]); + $user->posts = new DataCollection([new Post(['foo' => 'bar']), ['foo' => 'baz']]); + $this->assertInstanceOf(DataCollection::class, $user->posts); + $this->assertEquals('bar', $user->posts[0]->foo); + $this->assertEquals('baz', $user->posts[1]->foo); + $this->assertEquals(2, $user->posts->count()); + $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); + $this->assertEquals(['foo' => 'baz'], $user->toArray()['posts'][1]); + } + + public function testCollectionIsAdded() + { + $user = new User([]); + $user->posts = [new Post(['foo' => 'bar'])]; + $user->posts->add(new Post(['foo' => 'baz'])); + $this->assertEquals('baz', $user->posts[1]->foo); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testHasManyThrowsErrorWhenUnexpectedValueIsProvided() + { + $user = new User([]); + $user->posts = ['foo']; + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testHasManyThrowsErrorWhenNoCollectionIsProvided() + { + $user = new User([]); + $user->posts = 'foo'; + } +} diff --git a/tests/HasOneTest.php b/tests/HasOneTest.php new file mode 100644 index 0000000..eb8ad82 --- /dev/null +++ b/tests/HasOneTest.php @@ -0,0 +1,59 @@ + Settings::class]; + +} + +class HasOneTest extends TestCase +{ + public function testRelatedModelReturnsAsExpecteWhenInputIsArray() + { + $user = new Team(['settings' => ['foo' => 'bar']]); + $this->assertEquals('bar', $user->settings->foo); + } + + public function testRelatedModelReturnsAsExpecteWhenInputIsADataModel() + { + $user = new Team([]); + $user->settings = new Settings(['foo' => 'bar']); + $this->assertEquals('bar', $user->settings->foo); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testThrowErrorWhenSetValueIsUnexpected() + { + $user = new Team([]); + $user->settings = 'foo'; + } + + public function testRelatedObjectChangeAsExpected() + { + $user = new Team([]); + $user->settings = new Settings(['foo' => 'bar']); + $user->settings->baz = 'bazzer'; + $this->assertEquals('bar', $user->settings->foo); + $this->assertEquals('bazzer', $user->settings->baz); + } +} diff --git a/tests/ObjectModificationTest.php b/tests/ObjectModificationTest.php new file mode 100644 index 0000000..2db1c47 --- /dev/null +++ b/tests/ObjectModificationTest.php @@ -0,0 +1,74 @@ +toArray(); + } + return $value; + } + + public function cast($value) + { + return new DataCollection($value); + } +} + +class Menu extends DataModel { + + protected $casts = [ + 'sub_menus' => DataCollectionCaster::class + ]; + + protected $hasOne = [ + 'one_menu' => Menu::class + ]; + + protected $hasMany = [ + 'many_menus' => Menu::class + ]; + +} + +class ObjectModificationTest extends TestCase +{ + public function testObjectIsModifiedAfterAccessed() + { + $menu = new Menu([ + 'id' => 1, + 'sub_menus' => [ + new Menu(['id' => 2]) + ] + ]); + $menu->sub_menus->add(new Menu(['id' => 3])); + $this->assertEquals(2, $menu->sub_menus->count()); + $this->assertEquals(2, $menu->sub_menus[0]->id); + $this->assertEquals(3, $menu->sub_menus[1]->id); + } + + public function testHasOneRelationModificationTakesAffect() + { + $menu = new Menu(['id' => 1, 'one_menu' => ['id' => 2]]); + $menu->one_menu->id = 3; + $this->assertEquals(3, $menu->toArray()['one_menu']['id']); + } + + public function testHasManyRelationModificationTakesAffect() + { + $menu = new Menu(['id' => 1, 'many_menus' => [['id' => 2]]]); + $menu->many_menus->add(new Menu(['id' => 5])); + $menu->many_menus->add(new Menu(['id' => 7])); + $this->assertEquals(3, count($menu->toArray()['many_menus'])); + $this->assertEquals(2, $menu->toArray()['many_menus'][0]['id']); + $this->assertEquals(5, $menu->toArray()['many_menus'][1]['id']); + $this->assertEquals(7, $menu->toArray()['many_menus'][2]['id']); + } +}