├── .gitignore ├── .travis.yml ├── phpunit.xml ├── src └── CanGelis │ └── DataModels │ ├── Cast │ ├── FloatCast.php │ ├── IntegerCast.php │ ├── StringCast.php │ ├── BooleanCast.php │ ├── DateCast.php │ ├── Iso8601Cast.php │ ├── DateTimeCast.php │ └── AbstractCast.php │ ├── DataCollection.php │ ├── JsonModel.php │ ├── XmlModel.php │ └── DataModel.php ├── composer.json ├── LICENSE.md ├── tests ├── Json │ ├── AttributeTest.php │ ├── HasOneTest.php │ └── HasManyTest.php ├── DefaultValueTest.php ├── Xml │ ├── XmlHasManyTest.php │ ├── XmlAttributeTest.php │ └── XmlHasOneTest.php ├── ObjectModificationTest.php └── CastTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | 9 | before_script: 10 | - composer install 11 | 12 | script: vendor/bin/phpunit 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/Cast/FloatCast.php: -------------------------------------------------------------------------------- 1 | toDateString(); 24 | } 25 | return $value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/Cast/Iso8601Cast.php: -------------------------------------------------------------------------------- 1 | toIso8601String(); 24 | } 25 | return $value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/Cast/DateTimeCast.php: -------------------------------------------------------------------------------- 1 | toDateTimeString(); 24 | } 25 | return $value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cangelis/data-models", 3 | "description": "Data models is the beautiful way of working with structured data such as JSON and PHP arrays", 4 | "keywords": ["json", "mapper", "data", "dto", "xml", "array"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Can Geliş", 9 | "email": "geliscan@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^7||^6||^5", 17 | "nesbot/carbon": "^1||^2" 18 | }, 19 | "suggest": { 20 | "ext-json": "*", 21 | "ext-simplexml": "*" 22 | }, 23 | "autoload": { 24 | "psr-0": { 25 | "CanGelis\\": "src/" 26 | } 27 | }, 28 | "minimum-stability": "dev" 29 | } 30 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/Cast/AbstractCast.php: -------------------------------------------------------------------------------- 1 | 1]); 15 | $post->title = 'Foo'; 16 | $this->assertEquals(json_encode(['id' => 1, 'title' => 'Foo']), (string) $post); 17 | } 18 | 19 | public function testAttributeIsModified() 20 | { 21 | $post = new JsonModel(['id' => 1]); 22 | $post->id = 2; 23 | $this->assertEquals(json_encode(['id' => 2]), (string) $post); 24 | } 25 | 26 | public function testAttributeIsUnset() 27 | { 28 | $post = new JsonModel(['id' => 1]); 29 | unset($post->id); 30 | $this->assertEquals(json_encode([]), (string) $post); 31 | } 32 | 33 | public function testIsset() 34 | { 35 | $post = new JsonModel(['id' => 1]); 36 | $this->assertTrue(isset($post->id)); 37 | $this->assertFalse(isset($post->foo)); 38 | } 39 | } -------------------------------------------------------------------------------- /tests/DefaultValueTest.php: -------------------------------------------------------------------------------- 1 | FloatCast::class 18 | ]; 19 | 20 | protected $defaults = [ 21 | 'author' => 'Can Gelis', 22 | 'rate' => '0.0' 23 | ]; 24 | } 25 | 26 | class DefaultValueTest extends TestCase 27 | { 28 | public function testDefaultValueIsReturnedWhenItDoesntExist() 29 | { 30 | $comment = new Comment([]); 31 | $this->assertEquals('Can Gelis', $comment->author); 32 | } 33 | 34 | public function testDefaultValueIsNotReturnedWhenTheValueExists() 35 | { 36 | $comment = new Comment(['author' => 'Foo Bar']); 37 | $this->assertEquals('Foo Bar', $comment->author); 38 | } 39 | 40 | public function testDefaultValueIsNotReturnedWhenTheValuesIsNull() 41 | { 42 | $comment = new Comment(['author' => null]); 43 | $this->assertNull($comment->author); 44 | } 45 | 46 | public function testReturnsNullWhenItIsNotDefault() 47 | { 48 | $comment = new Comment([]); 49 | $this->assertNull($comment->text); 50 | } 51 | 52 | public function testDefaultIsCasted() 53 | { 54 | $comment = new Comment([]); 55 | $this->assertEquals(0.0, $comment->rate); 56 | $this->assertEquals('double', gettype($comment->rate)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Json/HasOneTest.php: -------------------------------------------------------------------------------- 1 | Settings::class]; 24 | 25 | } 26 | 27 | class HasOneTest extends TestCase 28 | { 29 | public function testRelatedModelReturnsAsExpecteWhenInputIsArray() 30 | { 31 | $user = new Team(['settings' => ['foo' => 'bar']]); 32 | $this->assertEquals('bar', $user->settings->foo); 33 | } 34 | 35 | public function testRelatedModelReturnsAsExpecteWhenInputIsADataModel() 36 | { 37 | $user = new Team([]); 38 | $user->settings = new Settings(['foo' => 'bar']); 39 | $this->assertEquals('bar', $user->settings->foo); 40 | } 41 | 42 | public function testRelationIsModified() 43 | { 44 | $team = new Team(['settings' => ['foo' => 'bar']]); 45 | $team->settings->foo = 'baz'; 46 | $this->assertEquals(json_encode(['settings' => ['foo' => 'baz']]), (string) $team); 47 | } 48 | 49 | /** 50 | * @expectedException \InvalidArgumentException 51 | */ 52 | public function testThrowErrorWhenSetValueIsUnexpected() 53 | { 54 | $user = new Team([]); 55 | $user->settings = 'foo'; 56 | } 57 | 58 | public function testRelatedObjectChangeAsExpected() 59 | { 60 | $user = new Team([]); 61 | $user->settings = new Settings(['foo' => 'bar']); 62 | $user->settings->baz = 'bazzer'; 63 | $this->assertEquals('bar', $user->settings->foo); 64 | $this->assertEquals('bazzer', $user->settings->baz); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Xml/XmlHasManyTest.php: -------------------------------------------------------------------------------- 1 | XmlPlayer::class]; 18 | 19 | } 20 | 21 | class XmlHasManyTest extends TestCase 22 | { 23 | public function testArrayOfInputIsSetAsExpected() 24 | { 25 | $team = XmlTeam::fromString(''); 26 | $team->players = [['name' => 'Beckham'], ['name' => 'Zidane']]; 27 | $this->assertContains('BeckhamZidane', (string) $team); 28 | $team->players->add(XmlPlayer::fromArray(['name' => 'Raul'])); 29 | $this->assertContains('BeckhamZidaneRaul', (string) $team); 30 | } 31 | 32 | public function testAddingHasManyXmlInputWorksAsExpected() 33 | { 34 | $team = XmlTeam::fromString('Beckham'); 35 | $this->assertContains('Beckham', (string) $team); 36 | $team->players->add(XmlPlayer::fromArray(['name' => 'Zidane'])); 37 | $this->assertContains('BeckhamZidane', (string) $team); 38 | } 39 | 40 | public function testSettingCollection() 41 | { 42 | $team = XmlTeam::fromString(''); 43 | $team->players = new DataCollection([XmlPlayer::fromArray(['name' => 'Zidane']), XmlPlayer::fromArray(['name' => 'Beckham'])]); 44 | $this->assertContains('ZidaneBeckham', (string) $team); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/ObjectModificationTest.php: -------------------------------------------------------------------------------- 1 | toArray(); 15 | } 16 | return $value; 17 | } 18 | 19 | public function cast($value) 20 | { 21 | return new DataCollection($value); 22 | } 23 | } 24 | 25 | class Menu extends JsonModel { 26 | 27 | protected $casts = [ 28 | 'sub_menus' => DataCollectionCaster::class 29 | ]; 30 | 31 | protected $hasOne = [ 32 | 'one_menu' => Menu::class 33 | ]; 34 | 35 | protected $hasMany = [ 36 | 'many_menus' => Menu::class 37 | ]; 38 | 39 | } 40 | 41 | class ObjectModificationTest extends TestCase 42 | { 43 | public function testObjectIsModifiedAfterAccessed() 44 | { 45 | $menu = new Menu([ 46 | 'id' => 1, 47 | 'sub_menus' => [ 48 | new Menu(['id' => 2]) 49 | ] 50 | ]); 51 | $menu->sub_menus->add(new Menu(['id' => 3])); 52 | $this->assertEquals(2, $menu->sub_menus->count()); 53 | $this->assertEquals(2, $menu->sub_menus[0]->id); 54 | $this->assertEquals(3, $menu->sub_menus[1]->id); 55 | } 56 | 57 | public function testHasOneRelationModificationTakesAffect() 58 | { 59 | $menu = new Menu(['id' => 1, 'one_menu' => ['id' => 2]]); 60 | $menu->one_menu->id = 3; 61 | $this->assertEquals(3, $menu->toArray()['one_menu']['id']); 62 | } 63 | 64 | public function testHasManyRelationModificationTakesAffect() 65 | { 66 | $menu = new Menu(['id' => 1, 'many_menus' => [['id' => 2]]]); 67 | $menu->many_menus->add(new Menu(['id' => 5])); 68 | $menu->many_menus->add(new Menu(['id' => 7])); 69 | $this->assertEquals(3, count($menu->toArray()['many_menus'])); 70 | $this->assertEquals(2, $menu->toArray()['many_menus'][0]['id']); 71 | $this->assertEquals(5, $menu->toArray()['many_menus'][1]['id']); 72 | $this->assertEquals(7, $menu->toArray()['many_menus'][2]['id']); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Xml/XmlAttributeTest.php: -------------------------------------------------------------------------------- 1 | '); 17 | $this->assertEquals('Foo', $post->title); 18 | } 19 | 20 | public function testAttributeCanBeModifiedAsExpected() 21 | { 22 | $post = XmlPost::fromString(''); 23 | $this->assertEquals('Foo', $post->title); 24 | $post->title = 'Bar'; 25 | $this->assertEquals('', (string) $post); 26 | } 27 | 28 | public function testNewAttributeCanBeAdded() 29 | { 30 | $post = XmlPost::fromString(''); 31 | $this->assertEquals('Foo', $post->title); 32 | $post->body = 'Bar'; 33 | $this->assertEquals('', (string) $post); 34 | } 35 | 36 | public function testNewChildCanBeAdded() 37 | { 38 | $post = XmlPost::fromString(''); 39 | $post->created_by = 'Foo Bar'; 40 | $this->assertEquals('Foo Bar', (string) $post); 41 | } 42 | 43 | public function testChildIsModified() 44 | { 45 | $post = XmlPost::fromString('Foo Bar'); 46 | $post->created_by = 'Baz Bazzer'; 47 | $this->assertEquals('Baz Bazzer', (string) $post); 48 | } 49 | 50 | public function testAttributeCanBeUnset() 51 | { 52 | $post = XmlPost::fromString(''); 53 | unset($post->title); 54 | $this->assertEquals('', (string) $post); 55 | } 56 | 57 | public function testChildCanBeUnset() 58 | { 59 | $post = XmlPost::fromString('Bar'); 60 | unset($post->name); 61 | $this->assertEquals('', (string) $post); 62 | } 63 | 64 | public function testIsset() 65 | { 66 | $post = XmlPost::fromString('Bar'); 67 | $this->assertTrue(isset($post->title)); 68 | $this->assertTrue(isset($post->name)); 69 | // not defined as an attribute so this should return false 70 | $this->assertFalse(isset($post->bar)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/DataCollection.php: -------------------------------------------------------------------------------- 1 | items = $items; 20 | } 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function offsetExists($offset) 26 | { 27 | return isset($this->items[$offset]); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function offsetGet($offset) 34 | { 35 | return $this->items[$offset]; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function offsetSet($offset, $value) 42 | { 43 | if (is_null($offset)) { 44 | $this->items[] = $value; 45 | } else { 46 | $this->items[$offset] = $value; 47 | } 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function offsetUnset($offset) 54 | { 55 | unset($this->items[$offset]); 56 | } 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | public function toJson() 62 | { 63 | return json_encode($this->items); 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | public function toArray() 70 | { 71 | return array_map(function ($item) { 72 | if ($item instanceof DataModel) { 73 | return $item->toArray(); 74 | } 75 | return $item; 76 | }, $this->items); 77 | } 78 | 79 | /** 80 | * Add an item to the collection. 81 | * 82 | * @param \CanGelis\DataModels\DataModel $item 83 | * 84 | * @return $this 85 | */ 86 | public function add(DataModel $item) 87 | { 88 | $this->items[] = $item; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * @inheritDoc 95 | */ 96 | public function getIterator() 97 | { 98 | return new \ArrayIterator($this->items); 99 | } 100 | 101 | /** 102 | * @inheritDoc 103 | */ 104 | public function count() 105 | { 106 | return count($this->items); 107 | } 108 | 109 | /** 110 | * Get the first item 111 | * 112 | * @param callable|null $callback 113 | * @param mixed $default 114 | * 115 | * @return mixed 116 | */ 117 | public function first(callable $callback = null, $default = null) 118 | { 119 | if (is_null($callback)) { 120 | $callback = function ($item) { 121 | return true; 122 | }; 123 | } 124 | 125 | foreach ($this->items as $item) { 126 | if ($callback($item)) { 127 | return $item; 128 | } 129 | } 130 | 131 | return $default; 132 | } 133 | 134 | /** 135 | * @inheritDoc 136 | */ 137 | public function filter(callable $callback) 138 | { 139 | return array_filter($this->items, $callback); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Xml/XmlHasOneTest.php: -------------------------------------------------------------------------------- 1 | DetailedXmlSettings::class 18 | ]; 19 | 20 | protected $attributes = ['blog_url']; 21 | 22 | } 23 | 24 | class XmlUser extends XmlModel { 25 | 26 | protected $root = 'user'; 27 | 28 | protected $hasOne = [ 29 | 'settings' => XmlSettings::class, 30 | 'different_settings' => XmlSettings::class 31 | ]; 32 | } 33 | 34 | class XmlHasOneTest extends TestCase 35 | { 36 | public function testRelationIsSetWhenInputIsArray() 37 | { 38 | $user = XmlUser::fromString(''); 39 | $user->settings = ['url' => 'https://foo.bar']; 40 | $this->assertEquals($user->settings->url, 'https://foo.bar'); 41 | $this->assertContains('https://foo.bar', (string)$user); 42 | $user->settings->foo = 'Bar'; 43 | $this->assertContains('https://foo.barBar', (string)$user); 44 | } 45 | 46 | public function testRelationIsSetWhenInputIsXmlModel() 47 | { 48 | $user = XmlUser::fromString(''); 49 | $settings = XmlSettings::fromString('Can'); 50 | $user->settings = $settings; 51 | $this->assertEquals($user->settings->name, 'Can'); 52 | $this->assertContains('Can', (string)$user); 53 | $user->settings->surname = 'Gelis'; 54 | $this->assertContains('CanGelis', (string)$user); 55 | } 56 | 57 | public function testRelationIsSetWhenInputIsXmlElement() 58 | { 59 | $user = XmlUser::fromString(''); 60 | $xmlSettings = new SimpleXMLElement('BarBazzer'); 61 | $user->settings = $xmlSettings; 62 | $this->assertEquals($user->settings->foo, 'Bar'); 63 | $this->assertContains('BazzerBar', (string)$user); 64 | $user->settings->bazzer = 'Fooer'; 65 | $this->assertContains('BazzerBarFooer', (string)$user); 66 | } 67 | 68 | public function testMultipleHasOneRelationships() 69 | { 70 | $user = XmlUser::fromString(''); 71 | $user->settings = ['foo' => 'bar']; 72 | $user->settings->detailed_settings = ['baz' => 'bazzer']; 73 | $this->assertContains('barbazzer', (string)$user); 74 | } 75 | 76 | public function testHasOneAttributesAreSetAsExpected() 77 | { 78 | $user = XmlUser::fromString(''); 79 | $user->settings = ['blog_url' => 'http://foo.bar', 'foo' => 'bar']; 80 | $this->assertContains('bar', (string) $user); 81 | } 82 | 83 | public function testRelationNameIsUsedForHasOneRelationships() 84 | { 85 | $user = XmlUser::fromString(''); 86 | $user->different_settings = ['foo' => 'bar']; 87 | $this->assertContains('bar', (string) $user); 88 | } 89 | 90 | public function testNoDuplicationInTheModifiedRelationship() 91 | { 92 | $user = XmlUser::fromString('bar'); 93 | $user->different_settings->baz = "bazzer"; 94 | $this->assertContains('barbazzer', (string) $user); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Json/HasManyTest.php: -------------------------------------------------------------------------------- 1 | Post::class 20 | ]; 21 | 22 | } 23 | 24 | class HasManyTest extends TestCase { 25 | 26 | public function testReturnCollectionWhenDataIsArray() 27 | { 28 | $user = new User(['posts' => [['foo' => 'bar'], ['foo' => 'baz']]]); 29 | $this->assertInstanceOf(DataCollection::class, $user->posts); 30 | $this->assertInstanceOf(Post::class, $user->posts->first()); 31 | $this->assertEquals(2, $user->posts->count()); 32 | } 33 | 34 | public function testRelationsAreModified() 35 | { 36 | $user = new User(['posts' => [['foo' => 'bar']]]); 37 | $user->posts[0]->foo = 'baz'; 38 | $this->assertEquals(json_encode(['posts' => [['foo' => 'baz']]]), (string) $user); 39 | } 40 | 41 | public function testReturnEmptyCollectionWhenAttributeDoesNotExist() 42 | { 43 | $user = new User([]); 44 | $this->assertInstanceOf(DataCollection::class, $user->posts); 45 | $this->assertEquals(0, $user->posts->count()); 46 | } 47 | 48 | public function testReturnEmptyCollectionWhenAttributeIsNotAnArray() 49 | { 50 | $user = new User(['posts' => null]); 51 | $this->assertInstanceOf(DataCollection::class, $user->posts); 52 | $this->assertEquals(0, $user->posts->count()); 53 | } 54 | 55 | public function testArrayValuesIsSetAsExceptedWhenItIsArrayOfArray() 56 | { 57 | $user = new User([]); 58 | $user->posts = [['foo' => 'bar']]; 59 | $this->assertInstanceOf(DataCollection::class, $user->posts); 60 | $this->assertEquals('bar', $user->posts->first()->foo); 61 | $this->assertEquals(1, $user->posts->count()); 62 | } 63 | 64 | public function testModelValuesAreSetAsExpectedWhenItIsArrayOfObjects() 65 | { 66 | $user = new User([]); 67 | $user->posts = [new Post(['foo' => 'bar'])]; 68 | $this->assertInstanceOf(DataCollection::class, $user->posts); 69 | $this->assertEquals('bar', $user->posts->first()->foo); 70 | $this->assertEquals(1, $user->posts->count()); 71 | $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); 72 | } 73 | 74 | public function testModelValuesAreSetAsExpectedWhenItIsArrayOfMixedTypes() 75 | { 76 | $user = new User([]); 77 | $user->posts = [new Post(['foo' => 'bar']), ['foo' => 'baz']]; 78 | $this->assertInstanceOf(DataCollection::class, $user->posts); 79 | $this->assertEquals('bar', $user->posts[0]->foo); 80 | $this->assertEquals('baz', $user->posts[1]->foo); 81 | $this->assertEquals(2, $user->posts->count()); 82 | $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); 83 | $this->assertEquals(['foo' => 'baz'], $user->toArray()['posts'][1]); 84 | } 85 | 86 | public function testModelValuesAreSetAsExpectedWhenValuesAreProvidedAsCollection() 87 | { 88 | $user = new User([]); 89 | $user->posts = new DataCollection([new Post(['foo' => 'bar']), new Post(['foo' => 'baz'])]); 90 | $this->assertInstanceOf(DataCollection::class, $user->posts); 91 | $this->assertEquals('bar', $user->posts[0]->foo); 92 | $this->assertEquals('baz', $user->posts[1]->foo); 93 | $this->assertEquals(2, $user->posts->count()); 94 | $this->assertEquals(['foo' => 'bar'], $user->toArray()['posts'][0]); 95 | $this->assertEquals(['foo' => 'baz'], $user->toArray()['posts'][1]); 96 | } 97 | 98 | public function testCollectionIsAdded() 99 | { 100 | $user = new User([]); 101 | $user->posts = [new Post(['foo' => 'bar'])]; 102 | $user->posts->add(new Post(['foo' => 'baz'])); 103 | $this->assertEquals('baz', $user->posts[1]->foo); 104 | } 105 | 106 | /** 107 | * @expectedException \InvalidArgumentException 108 | */ 109 | public function testHasManyThrowsErrorWhenUnexpectedValueIsProvided() 110 | { 111 | $user = new User([]); 112 | $user->posts = ['foo']; 113 | } 114 | 115 | /** 116 | * @expectedException \InvalidArgumentException 117 | */ 118 | public function testHasManyThrowsErrorWhenNoCollectionIsProvided() 119 | { 120 | $user = new User([]); 121 | $user->posts = 'foo'; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/JsonModel.php: -------------------------------------------------------------------------------- 1 | data = $data; 20 | } 21 | 22 | /** 23 | * Make an instance from a string 24 | * 25 | * @param string $json 26 | * 27 | * @return static 28 | */ 29 | public static function fromString($json) 30 | { 31 | return new static(json_decode($json, true)); 32 | } 33 | 34 | /** 35 | * Make an array 36 | * 37 | * @return array 38 | */ 39 | public function toArray() 40 | { 41 | $data = $this->data; 42 | 43 | // apply modified relationships 44 | foreach ($this->relations as $relationAttribute => $relation) { 45 | list($relationType, $attribute) = explode("-", $relationAttribute); 46 | $data[$attribute] = $relation->toArray(); 47 | } 48 | 49 | foreach ($this->attributeValues as $attribute => $value) { 50 | $data[$attribute] = $this->uncastValue($attribute, $value); 51 | } 52 | 53 | return $data; 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function jsonSerialize() 60 | { 61 | return $this->toArray(); 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function __toString() 68 | { 69 | return $this->toJson(); 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public function toJson() 76 | { 77 | return json_encode($this->toArray()); 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function __isset($name) 84 | { 85 | return isset($this->data[$name]); 86 | } 87 | 88 | /** 89 | * @inheritDoc 90 | */ 91 | public function __unset($name) 92 | { 93 | unset($this->data[$name]); 94 | unset($this->relations[$name]); 95 | unset($this->attributeValues[$name]); 96 | } 97 | 98 | /** 99 | * Make item a data model 100 | * 101 | * @param array|\CanGelis\DataModels\DataModel $item 102 | * @param string $class 103 | * 104 | * @return \CanGelis\DataModels\DataModel 105 | */ 106 | protected function getItemAsObject($item, $class) 107 | { 108 | if (is_array($item)) { 109 | return new $class($item); 110 | } 111 | 112 | if (is_object($item) && get_class($item) == $class) { 113 | return $item; 114 | } 115 | 116 | throw new \InvalidArgumentException('Expected array or ' . $class . ' but ' . gettype($item) . ' given'); 117 | } 118 | 119 | /** 120 | * @inheritDoc 121 | */ 122 | protected function setHasOne($relation, $value) 123 | { 124 | return $this->getItemAsObject($value, $this->hasOne[$relation]); 125 | } 126 | 127 | /** 128 | * @inheritDoc 129 | */ 130 | protected function setHasMany($relation, $value) 131 | { 132 | if (is_array($value)) { 133 | $collection = $this->makeCollection([]); 134 | foreach ($value as $item) { 135 | $collection->add($this->getItemAsObject($item, $this->hasMany[$relation])); 136 | } 137 | return $collection; 138 | } 139 | 140 | if ($value instanceof DataCollection) { 141 | return $value; 142 | } 143 | 144 | throw new \InvalidArgumentException('Expected array or DataCollection but ' . gettype($value) . ' given'); 145 | } 146 | 147 | /** 148 | * @inheritDoc 149 | */ 150 | protected function resolveHasManyRelationship($relation) 151 | { 152 | $items = []; 153 | 154 | if (array_key_exists($relation, $this->data) && is_array($this->data[$relation])) { 155 | $items = $this->data[$relation]; 156 | } 157 | 158 | unset($this->data[$relation]); 159 | 160 | return $this->makeCollection( 161 | array_map(function ($item) use ($relation) { 162 | return $this->getItemAsObject($item, $this->hasMany[$relation]); 163 | }, $items) 164 | ); 165 | } 166 | 167 | /** 168 | * @inheritDoc 169 | */ 170 | protected function resolveHasOneRelationship($relation) 171 | { 172 | if (is_array($this->data[$relation])) { 173 | $model = new $this->hasOne[$relation]($this->data[$relation]); 174 | unset($this->data[$relation]); 175 | return $model; 176 | } 177 | 178 | return null; 179 | } 180 | 181 | /** 182 | * @inheritDoc 183 | */ 184 | protected function hasAttribute($attribute) 185 | { 186 | return array_key_exists($attribute, $this->data); 187 | } 188 | 189 | /** 190 | * @inheritDoc 191 | */ 192 | protected function getAttribute($attribute) 193 | { 194 | return $this->data[$attribute]; 195 | } 196 | 197 | /** 198 | * @inheritDoc 199 | */ 200 | protected function onLoadAttribute($attribute) 201 | { 202 | unset($this->data[$attribute]); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tests/CastTest.php: -------------------------------------------------------------------------------- 1 | FloatCast::class, 26 | 'age' => IntegerCast::class, 27 | 'has_license' => BooleanCast::class, 28 | 'license_number' => StringCast::class, 29 | 'birth_date' => DateCast::class, 30 | 'created_at' => DateTimeCast::class, 31 | 'updated_at' => Iso8601Cast::class 32 | ]; 33 | 34 | } 35 | 36 | class CastTest extends TestCase 37 | { 38 | public function testBoolean() 39 | { 40 | $player = new Player(['has_license' => 'false']); 41 | $this->assertEquals('boolean', gettype($player->has_license)); 42 | $this->assertFalse($player->has_license); 43 | $player = new Player(['has_license' => null]); 44 | $this->assertEquals('boolean', gettype($player->has_license)); 45 | $this->assertFalse($player->has_license); 46 | $player = new Player(['has_license' => false]); 47 | $this->assertEquals('boolean', gettype($player->has_license)); 48 | $this->assertFalse($player->has_license); 49 | $player = new Player(['has_license' => 0]); 50 | $this->assertEquals('boolean', gettype($player->has_license)); 51 | $this->assertFalse($player->has_license); 52 | $player = new Player(['has_license' => 'true']); 53 | $this->assertEquals('boolean', gettype($player->has_license)); 54 | $this->assertTrue($player->has_license); 55 | $player = new Player(['has_license' => true]); 56 | $this->assertEquals('boolean', gettype($player->has_license)); 57 | $this->assertTrue($player->has_license); 58 | $player = new Player(['has_license' => 1]); 59 | $this->assertEquals('boolean', gettype($player->has_license)); 60 | $this->assertTrue($player->has_license); 61 | } 62 | 63 | public function testInteger() 64 | { 65 | $player = new Player(['age' => 10]); 66 | $this->assertEquals('integer', gettype($player->age)); 67 | $this->assertEquals(10, $player->age); 68 | $player = new Player(['age' => '10']); 69 | $this->assertEquals('integer', gettype($player->age)); 70 | $this->assertEquals(10, $player->age); 71 | $player = new Player(['age' => '10.0']); 72 | $this->assertEquals('integer', gettype($player->age)); 73 | $this->assertEquals(10, $player->age); 74 | $player = new Player(['age' => 10.0]); 75 | $this->assertEquals('integer', gettype($player->age)); 76 | $this->assertEquals(10, $player->age); 77 | } 78 | 79 | public function testString() 80 | { 81 | $player = new Player(['license_number' => 1234]); 82 | $this->assertEquals('string', gettype($player->license_number)); 83 | $this->assertEquals('1234', $player->license_number); 84 | $player = new Player(['license_number' => '1234']); 85 | $this->assertEquals('string', gettype($player->license_number)); 86 | $this->assertEquals('1234', $player->license_number); 87 | $player = new Player(['license_number' => null]); 88 | $this->assertEquals('string', gettype($player->license_number)); 89 | $this->assertEquals('', $player->license_number); 90 | } 91 | 92 | public function testFloat() 93 | { 94 | $player = new Player(['rate' => 10]); 95 | $this->assertEquals('double', gettype($player->rate)); 96 | $this->assertEquals(10.0, $player->rate); 97 | $player = new Player(['rate' => '10']); 98 | $this->assertEquals('double', gettype($player->rate)); 99 | $this->assertEquals(10.0, $player->rate); 100 | $player = new Player(['rate' => '10.1']); 101 | $this->assertEquals('double', gettype($player->rate)); 102 | $this->assertEquals(10.1, $player->rate); 103 | $player = new Player(['rate' => 10.1]); 104 | $this->assertEquals('double', gettype($player->rate)); 105 | $this->assertEquals(10.1, $player->rate); 106 | $player = new Player(['rate' => null]); 107 | $this->assertEquals('double', gettype($player->rate)); 108 | $this->assertEquals(0.0, $player->rate); 109 | } 110 | 111 | public function testDate() 112 | { 113 | $player = new Player(['birth_date' => '1990-07-18']); 114 | $this->assertEquals(Carbon::class, get_class($player->birth_date)); 115 | $player->birth_date = $now = Carbon::now(); 116 | $this->assertEquals(Carbon::class, get_class($player->birth_date)); 117 | $this->assertEquals($now->year, $player->birth_date->year); 118 | $this->assertEquals($now->month, $player->birth_date->month); 119 | $this->assertEquals($now->day, $player->birth_date->day); 120 | $this->assertEquals($now->toDateString(), $player->toArray()['birth_date']); 121 | } 122 | 123 | public function testDateTime() 124 | { 125 | $player = new Player(['created_at' => '2019-01-03 12:13:14']); 126 | $this->assertEquals(Carbon::class, get_class($player->created_at)); 127 | $player->created_at = $now = Carbon::now(); 128 | $this->assertEquals(Carbon::class, get_class($player->created_at)); 129 | $this->assertEquals($now->year, $player->created_at->year); 130 | $this->assertEquals($now->month, $player->created_at->month); 131 | $this->assertEquals($now->day, $player->created_at->day); 132 | $this->assertEquals($now->toDateTimeString(), $player->toArray()['created_at']); 133 | } 134 | 135 | public function testIso8601() 136 | { 137 | $player = new Player(['updated_at' => '2018-11-11T12:58:27+09:00']); 138 | $this->assertEquals(Carbon::class, get_class($player->updated_at)); 139 | $player->updated_at = $now = Carbon::now(); 140 | $this->assertEquals(Carbon::class, get_class($player->updated_at)); 141 | $this->assertEquals($now->year, $player->updated_at->year); 142 | $this->assertEquals($now->month, $player->updated_at->month); 143 | $this->assertEquals($now->day, $player->updated_at->day); 144 | $this->assertEquals($now->offsetHours, $player->updated_at->offsetHours); 145 | $this->assertEquals($now->toIso8601String(), $player->toArray()['updated_at']); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/CanGelis/DataModels/XmlModel.php: -------------------------------------------------------------------------------- 1 | root; 33 | } 34 | 35 | if (is_null($data)) { 36 | $data = new \SimpleXMLElement('<' . $root . '>'); 37 | } 38 | 39 | $this->data = clone $data; 40 | } 41 | 42 | /** 43 | * Initialize from XML string 44 | * 45 | * @param string $data 46 | * @param string $root 47 | * 48 | * @return static 49 | */ 50 | public static function fromString($data, $root = null) 51 | { 52 | return new static(simplexml_load_string($data), $root); 53 | } 54 | 55 | /** 56 | * Make an instance from an array 57 | * 58 | * @param array $data 59 | * @param string $root 60 | * 61 | * @return static 62 | */ 63 | public static function fromArray(array $data, $root = null) 64 | { 65 | $instance = new static(null, $root); 66 | foreach ($data as $key => $value) { 67 | $instance->{$key} = $value; 68 | } 69 | return $instance; 70 | } 71 | 72 | /** 73 | * Get the xml element 74 | * 75 | * @return \SimpleXMLElement 76 | */ 77 | public function toXMLElement() 78 | { 79 | $xmlElement = clone $this->data; 80 | 81 | // resolve dynamically loaded attribute values 82 | foreach ($this->attributeValues as $attribute => $value) { 83 | $value = $this->uncastValue($attribute, $value); 84 | if (in_array($attribute, $this->attributes)) { 85 | $xmlElement[$attribute] = $value; 86 | } else { 87 | $xmlElement->addChild($attribute, $value); 88 | } 89 | } 90 | 91 | // resolve dynamic has many relations 92 | foreach ($this->relations as $relationAttribute => $value) { 93 | list($relationType, $relation) = explode("-", $relationAttribute); 94 | if ($relationType == 'hasOne') { 95 | static::addChild($xmlElement, $value->toXMLElement()); 96 | } else { 97 | $parent = new \SimpleXMLElement('<' . $relation . '>'); 98 | static::addChild($xmlElement, $parent); 99 | foreach ($value as $xmlModel) { 100 | static::addChild($xmlElement->{$relation}, $xmlModel->toXMLElement()); 101 | } 102 | } 103 | } 104 | 105 | return $xmlElement; 106 | } 107 | 108 | /** 109 | * @inheritDoc 110 | */ 111 | public function __toString() 112 | { 113 | return $this->toXMLElement()->asXML(); 114 | } 115 | 116 | /** 117 | * Add a child to an xml element 118 | * 119 | * @param \SimpleXMLElement $root 120 | * @param \SimpleXMLElement $child 121 | */ 122 | public static function addChild(\SimpleXMLElement $root, \SimpleXMLElement $child) 123 | { 124 | $node = $root->addChild($child->getName(), (string) $child); 125 | foreach($child->attributes() as $attr => $value) { 126 | $node->addAttribute($attr, $value); 127 | } 128 | foreach($child->children() as $ch) { 129 | static::addChild($node, $ch); 130 | } 131 | } 132 | 133 | /** 134 | * @inheritDoc 135 | */ 136 | public function __unset($name) 137 | { 138 | if (!in_array($name, $this->attributes)) { 139 | unset($this->data->{$name}); 140 | } else { 141 | unset($this->data[$name]); 142 | } 143 | unset($this->relations[$name]); 144 | unset($this->attributeValues[$name]); 145 | } 146 | 147 | /** 148 | * @inheritDoc 149 | */ 150 | public function __isset($attribute) 151 | { 152 | if (!in_array($attribute, $this->attributes)) { 153 | return isset($this->data->{$attribute}); 154 | } 155 | 156 | return isset($this->data[$attribute]); 157 | } 158 | 159 | /** 160 | * @inheritDoc 161 | */ 162 | protected function resolveHasOneRelationship($relation) 163 | { 164 | if (isset($this->data->{$relation})) { 165 | $model = new $this->hasOne[$relation]($this->data->{$relation}, $relation); 166 | unset($this->data->{$relation}); 167 | return $model; 168 | } 169 | 170 | return null; 171 | } 172 | 173 | /** 174 | * @inheritDoc 175 | */ 176 | protected function resolveHasManyRelationship($relation) 177 | { 178 | $items = []; 179 | foreach ($this->data->{$relation}->children() as $child) { 180 | $items[] = new $this->hasMany[$relation]($child, $child->getName()); 181 | } 182 | 183 | unset($this->data->{$relation}); 184 | 185 | return $this->makeCollection($items); 186 | } 187 | 188 | /** 189 | * @inheritDoc 190 | */ 191 | protected function setHasOne($relation, $value) 192 | { 193 | $relatedClass = $this->hasOne[$relation]; 194 | if (is_array($value)) { 195 | return $relatedClass::fromArray($value, $relation); 196 | } 197 | 198 | if ($value instanceof XmlModel) { 199 | return $value; 200 | } 201 | 202 | if ($value instanceof \SimpleXMLElement) { 203 | return new $relatedClass($value, $relation); 204 | } 205 | } 206 | 207 | /** 208 | * @inheritDoc 209 | */ 210 | protected function setHasMany($relation, $values) 211 | { 212 | unset($this->data->{$relation}); 213 | $collection = $this->makeCollection([]); 214 | 215 | foreach ($values as $value) { 216 | $class = $this->hasMany[$relation]; 217 | 218 | if (is_array($value)) { 219 | $collection->add($class::fromArray($value)); 220 | } 221 | 222 | if ($value instanceof XmlModel) { 223 | $collection->add($value); 224 | } 225 | 226 | if ($value instanceof \SimpleXMLElement) { 227 | $collection->add(new $class($value, $value->getName())); 228 | } 229 | } 230 | 231 | return $collection; 232 | } 233 | 234 | /** 235 | * Get the attribute 236 | * 237 | * @param $attribute 238 | * 239 | * @return mixed|\SimpleXMLElement 240 | */ 241 | protected function getAttribute($attribute) 242 | { 243 | if (!in_array($attribute, $this->attributes)) { 244 | return isset($this->data->{$attribute}) ? (string)$this->data->{$attribute} : null; 245 | } 246 | 247 | return isset($this->data[$attribute]) ? (string)$this->data[$attribute] : null; 248 | } 249 | 250 | /** 251 | * @inheritDoc 252 | */ 253 | protected function hasAttribute($attribute) 254 | { 255 | if (!in_array($attribute, $this->attributes)) { 256 | return isset($this->data->{$attribute}); 257 | } 258 | 259 | return isset($this->data[$attribute]); 260 | } 261 | 262 | /** 263 | * @inheritDoc 264 | */ 265 | protected function onLoadAttribute($attribute) 266 | { 267 | if (!in_array($attribute, $this->attributes)) { 268 | unset($this->data->{$attribute}); 269 | } else { 270 | unset($this->data[$attribute]); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data models 2 | 3 | [![Build Status](https://travis-ci.org/cangelis/data-models.svg?branch=master)](https://travis-ci.org/cangelis/data-models) 4 | 5 | Data models is the beautiful way of working with structured data such as JSON, XML and php arrays. They are basically wrapper classes to the JSON and XML strings or php arrays. Models simplify the manipulation and processing workflow of the JSON, XML or php arrays. 6 | 7 | ## Pros 8 | 9 | - Straightforward to get started (this page will tell you all the features) 10 | - Avoid undefined index by design 11 | - Dynamic access to the model properties so no need of mapping the class properties with JSON or XML attributes 12 | - IDE auto-completion using `@property` docblock and make the API usage documented by default 13 | - Has many and has one relationships between models 14 | - Ability to assign default values for the attributes so the undefined attributes can be handled reliably 15 | - Ability to add logic into the data in the model 16 | - Cast values to known types such as integer, string, float, boolean 17 | - Cast values to Carbon object to work on date attributes easily 18 | - Ability to implement custom cast types 19 | - Manipulate and work on the object models instead of arrays and make them array or serialize to JSON back 20 | 21 | ## Install 22 | 23 | composer require cangelis/data-models:^2.0 24 | 25 | ## JSON Usage 26 | 27 | Imagine you have a JSON data for a blog post looks like this 28 | 29 | ``` 30 | $data = '{ 31 | "id": 1, 32 | "author": "Can Gelis", 33 | "created_at": "2019-05-11 22:00:00", 34 | "comments": [ 35 | { 36 | "id": 1, 37 | "text": "Hello World!" 38 | }, 39 | { 40 | "id": 2, 41 | "text": "What a wonderful world!" 42 | } 43 | ], 44 | "settings": {"comments_enable": 1} 45 | }'; 46 | ``` 47 | 48 | You can create the models looks like this 49 | 50 | ```php 51 | 52 | use CanGelis\DataModels\JsonModel; 53 | use CanGelis\DataModels\Cast\BooleanCast; 54 | use CanGelis\DataModels\Cast\DateTimeCast; 55 | 56 | /** 57 | * Define docblock for ide auto-completion 58 | * 59 | * @property bool $comments_enable 60 | */ 61 | class Settings extends JsonModel { 62 | 63 | protected $casts = ['comments_enable' => BooleanCast::class]; 64 | 65 | protected $defaults = ['comments_enable' => false]; 66 | 67 | } 68 | 69 | /** 70 | * Define docblock for ide auto-completion 71 | * 72 | * @property integer $id 73 | * @property string $text 74 | */ 75 | class Comment extends JsonModel {} 76 | 77 | /** 78 | * Define docblock for ide auto-completion 79 | * 80 | * @property integer $id 81 | * @property author $text 82 | * @property Carbon\Carbon $created_at 83 | * @property Settings $settings 84 | * @property CanGelis\DataModels\DataCollection $comments 85 | */ 86 | class Post extends JsonModel { 87 | 88 | protected $defaults = ['text' => 'No Text']; 89 | 90 | protected $casts = ['created_at' => DateTimeCast::class]; 91 | 92 | protected $hasMany = ['comments' => Comment::class]; 93 | 94 | protected $hasOne = ['settings' => Settings::class]; 95 | 96 | } 97 | 98 | ``` 99 | 100 | Use the models 101 | 102 | ```php 103 | 104 | $post = Post::fromString($data); // initialize from JSON String 105 | $post = new Post(json_decode($data, true)); // or use arrays 106 | 107 | $post->text // "No Text" in $defaults 108 | $post->foo // returns null which doesn't have default value 109 | $post->created_at // get Carbon object 110 | $post->created_at->addDay(1) // Go to tomorrow 111 | $post->created_at = Carbon::now() // update the creation time 112 | 113 | $post->settings->comments_enable // returns true 114 | $post->settings->comments_enable = false // manipulate the object 115 | $post->settings->comments_enable // returns false 116 | $post->settings->editable = false // introduce a new attribute 117 | 118 | $post->comments->first() // returns the first comment 119 | $post->comments[1] // get the second comment 120 | foreach ($post->comments as $comment) {} // iterate on comments 121 | $post->comments->add(new Comment(['id' => 3, 'text' => 'Not too bad'])) // add to the collection 122 | 123 | $post->toArray() // see as array 124 | $post->toJson() // serialize to json 125 | 126 | /* 127 | {"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}} 128 | */ 129 | 130 | ``` 131 | 132 | ## XML Usage 133 | 134 | It is pretty straightforward and very similar to JSON models. 135 | 136 | Imagine an XML data: 137 | 138 | ```php 139 | $data = ' 140 | 141 | Beckham1975-05-02 142 | Zidane1972-06-23 143 | 144 | 145 | Istanbul 146 | Turkey 147 | 148 | '; 149 | ``` 150 | 151 | You can setup a relationship looks like this: 152 | 153 | ```php 154 | use CanGelis\DataModels\XmlModel; 155 | use CanGelis\DataModels\Cast\DateCast; 156 | 157 | class Player extends XmlModel { 158 | 159 | // root tag name 160 | protected $root = 'Player'; 161 | 162 | protected $casts = ['BirthDate' => DateCast::class]; 163 | 164 | } 165 | 166 | class Address extends Xmlmodel { 167 | 168 | protected $root = 'Address'; 169 | 170 | } 171 | 172 | class Team extends XmlModel { 173 | 174 | protected $root = 'Team'; 175 | 176 | protected $hasMany = [ 177 | 'Players' => Player::class 178 | ]; 179 | 180 | protected $hasOne = [ 181 | 'TeamLocation' => Address::class 182 | ]; 183 | 184 | // the attributes in this array will be 185 | // behave as XML attributes see the example 186 | protected $attributes = ['Color']; 187 | 188 | } 189 | ``` 190 | 191 | Once you setup the relationships and your data, you start using the data. 192 | 193 | ```php 194 | $team = Team::fromString($data); 195 | 196 | echo $team->TeamLocation->City; // returns Istanbul 197 | $team->TeamLocation->City = 'Madrid'; // update the city 198 | 199 | echo $team->Players->count(); // number of players 200 | echo $team->Players[0]->Name; // gets first player's name 201 | 202 | echo $team->Color; // gets the Color XML attribute 203 | $team->Color = '#000000'; // update the XML Attribute 204 | 205 | echo get_class($team->Players[0]->BirthDate); // returns Carbon\Carbon 206 | $team->Players->add(Player::fromArray(['Name' => 'Ronaldinho'])); // add a new player 207 | 208 | echo (string) $team; // make an xml string 209 | ``` 210 | 211 | The resulting XML will be; 212 | 213 | ```xml 214 | 215 | 216 | Turkey 217 | Madrid 218 | 219 | 220 | Beckham1975-05-02 221 | Zidane1972-06-23 222 | Ronaldinho 223 | 224 | 225 | ``` 226 | 227 | 228 | ## Available Casts 229 | 230 | Here are the available casts. 231 | 232 | ```php 233 | 234 | CanGelis\DataModels\Cast\BooleanCast 235 | CanGelis\DataModels\Cast\FloatCast 236 | CanGelis\DataModels\Cast\IntegerCast 237 | CanGelis\DataModels\Cast\StringCast 238 | // these require nesbot/carbon package to work 239 | CanGelis\DataModels\Cast\DateCast 240 | CanGelis\DataModels\Cast\DateTimeCast 241 | CanGelis\DataModels\Cast\Iso8601Cast 242 | 243 | ``` 244 | 245 | ## Custom Casts 246 | 247 | If you prefer to implement more complex value casting logic, data models allow you to implement your custom ones. 248 | 249 | Imagine you use Laravel Eloquent and want to cast an in a JSON attribute. 250 | 251 | ```php 252 | 253 | // data = {"id": 1, "user": 1} 254 | 255 | class EloquentUserCast extends AbstractCast { 256 | 257 | /** 258 | * The value is casted when it is accessed 259 | * So this is a good place to convert the value in the 260 | * JSON into what we'd like to see 261 | * 262 | * @param mixed $value 263 | * 264 | * @return mixed 265 | */ 266 | public function cast($value) 267 | { 268 | if (!$value instanceof User) { 269 | return User::find($value); 270 | } 271 | return $value; 272 | } 273 | 274 | /** 275 | * This method is called when the object is serialized back to 276 | * array or JSON 277 | * So this is good place to make the values 278 | * json compatible such as integer, string or bool 279 | * 280 | * @param mixed $value 281 | * 282 | * @return mixed 283 | */ 284 | public function uncast($value) 285 | { 286 | if ($value instanceof User) { 287 | return $value->id; 288 | } 289 | return $value; 290 | } 291 | } 292 | 293 | class Post { 294 | 295 | protected $casts = ['user' => EloquentUserCast::class]; 296 | 297 | } 298 | 299 | $post->user = User::find(2); // set the Eloquent model directly 300 | $post->user = 2; // set only the id instead 301 | $post->user // returns instance of User 302 | $post->toArray() 303 | 304 | ['id' => 1, 'user' => 2] 305 | 306 | ``` 307 | ## Contribution 308 | 309 | Feel free to contribute! -------------------------------------------------------------------------------- /src/CanGelis/DataModels/DataModel.php: -------------------------------------------------------------------------------- 1 | hasMany)) { 60 | return $this->getHasManyValue($attribute); 61 | } 62 | 63 | // resolve has one relationship 64 | if (array_key_exists($attribute, $this->hasOne)) { 65 | return $this->getHasOneValue($attribute); 66 | } 67 | 68 | // return if it was accessed before 69 | if (array_key_exists($attribute, $this->attributeValues)) { 70 | return $this->attributeValues[$attribute]; 71 | } 72 | 73 | if ($this->hasAttribute($attribute)) { 74 | return $this->loadAttribute($attribute, $this->getAttribute($attribute)); 75 | } 76 | 77 | if (array_key_exists($attribute, $this->getDefaults())) { 78 | return $this->loadAttribute($attribute, $this->getDefaults()[$attribute]); 79 | } 80 | 81 | return $this->loadAttribute($attribute, null); 82 | } 83 | 84 | /** 85 | * Set the value 86 | * 87 | * @param string $attribute 88 | * @param mixed $value 89 | * 90 | * @throws \InvalidArgumentException 91 | */ 92 | public function __set($attribute, $value) 93 | { 94 | if (array_key_exists($attribute, $this->hasOne)) { 95 | $this->setHasOneValue($attribute, $value); 96 | } elseif (array_key_exists($attribute, $this->hasMany)) { 97 | $this->setHasManyValue($attribute, $value); 98 | } else { 99 | $this->loadAttribute($attribute, $value); 100 | } 101 | } 102 | 103 | /** 104 | * Load the attribute 105 | * 106 | * @param string $attribute 107 | * @param mixed $value 108 | * 109 | * @return mixed 110 | */ 111 | protected function loadAttribute($attribute, $value) 112 | { 113 | $this->attributeValues[$attribute] = $this->castValue($attribute, $value); 114 | // if the value is already in the source data 115 | // it should be unset since we load the value into attributeValues 116 | // to avoid duplication 117 | $this->onLoadAttribute($attribute); 118 | 119 | return $this->attributeValues[$attribute]; 120 | } 121 | 122 | /** 123 | * Get has many relationship value 124 | * 125 | * @param mixed $relation 126 | * 127 | * @return \CanGelis\DataModels\DataCollection 128 | */ 129 | protected function getHasManyValue($relation) 130 | { 131 | if ($this->isHasManyRelationLoaded($relation)) { 132 | return $this->getLoadedHasManyRelationValue($relation); 133 | } 134 | 135 | return $this->relations['hasMany-' . $relation] = $this->resolveHasManyRelationShip($relation); 136 | } 137 | 138 | /** 139 | * Get the already loaded has many relation value 140 | * 141 | * @param string $relation 142 | * 143 | * @return mixed 144 | */ 145 | protected function getLoadedHasManyRelationValue($relation) 146 | { 147 | return $this->relations['hasMany-' . $relation]; 148 | } 149 | 150 | /** 151 | * Get the already loaded has one relation value 152 | * 153 | * @param string $relation 154 | * 155 | * @return mixed 156 | */ 157 | protected function getLoadedHasOneRelationValue($relation) 158 | { 159 | return $this->relations['hasOne-' . $relation]; 160 | } 161 | 162 | /** 163 | * Returns true if the given has many relation is already loaded 164 | * 165 | * @param string $relation 166 | * 167 | * @return bool 168 | */ 169 | protected function isHasManyRelationLoaded($relation) 170 | { 171 | return isset($this->relations['hasMany-' . $relation]); 172 | } 173 | 174 | /** 175 | * Returns true if the given has one relation is already loaded 176 | * 177 | * @param string $relation 178 | * 179 | * @return bool 180 | */ 181 | protected function isHasOneRelationLoaded($relation) 182 | { 183 | return isset($this->relations['hasOne-' . $relation]); 184 | } 185 | 186 | /** 187 | * Get the has one relationship value 188 | * 189 | * @param mixed $attribute 190 | * 191 | * @return \CanGelis\DataModels\DataModel|null 192 | */ 193 | protected function getHasOneValue($attribute) 194 | { 195 | if ($this->isHasOneRelationLoaded($attribute)) { 196 | return $this->getLoadedHasOneRelationValue($attribute); 197 | } 198 | 199 | return $this->relations['hasOne-' . $attribute] = $this->resolveHasOneRelationship($attribute); 200 | } 201 | 202 | /** 203 | * Set has one value 204 | * 205 | * @param string $attribute 206 | * @param array|\CanGelis\DataModels\DataModel $value 207 | */ 208 | protected function setHasOneValue($attribute, $value) 209 | { 210 | $this->relations['hasOne-' . $attribute] = $this->setHasOne($attribute, $value); 211 | } 212 | 213 | /** 214 | * Set has many value 215 | * 216 | * @param string $attribute 217 | * @param \CanGelis\DataModels\DataCollection $value 218 | */ 219 | protected function setHasManyValue($attribute, $value) 220 | { 221 | $this->relations['hasMany-' . $attribute] = $this->setHasMany($attribute, $value); 222 | } 223 | 224 | /** 225 | * Default values for the attributes that doesn't exist 226 | * in the data, don't hesitate to override this if you have 227 | * more complex defaults logic 228 | * 229 | * @return array 230 | */ 231 | protected function getDefaults() 232 | { 233 | return $this->defaults; 234 | } 235 | 236 | /** 237 | * Cast an attribute value 238 | * 239 | * @param string $attribute 240 | * @param string $value 241 | * 242 | * @return mixed 243 | */ 244 | protected function castValue($attribute, $value) 245 | { 246 | if (!array_key_exists($attribute, $this->casts)) { 247 | return $value; 248 | } 249 | 250 | /** 251 | * @var \CanGelis\DataModels\Cast\AbstractCast $caster 252 | */ 253 | $caster = new $this->casts[$attribute](); 254 | 255 | return $caster->cast($value); 256 | } 257 | 258 | /** 259 | * Revert casted value back to the serialiazable form 260 | * 261 | * @param string $attribute 262 | * @param mixed $value 263 | * 264 | * @return mixed 265 | */ 266 | protected function uncastValue($attribute, $value) 267 | { 268 | if (!array_key_exists($attribute, $this->casts)) { 269 | return $value; 270 | } 271 | 272 | /** 273 | * @var \CanGelis\DataModels\Cast\AbstractCast $caster 274 | */ 275 | $caster = new $this->casts[$attribute](); 276 | 277 | return $caster->uncast($value); 278 | } 279 | 280 | /** 281 | * Make a new collection 282 | * 283 | * @param array $items 284 | * 285 | * @return \CanGelis\DataModels\DataCollection 286 | */ 287 | protected function makeCollection($items) 288 | { 289 | return new DataCollection($items); 290 | } 291 | 292 | /** 293 | * Resolve has many relationship 294 | * 295 | * @param string $relation 296 | * 297 | * @return \CanGelis\DataModels\DataCollection 298 | */ 299 | abstract protected function resolveHasManyRelationship($relation); 300 | 301 | /** 302 | * Resolve has one relationship 303 | * 304 | * @param string $relation 305 | * 306 | * @return \CanGelis\DataModels\DataModel 307 | */ 308 | abstract protected function resolveHasOneRelationship($relation); 309 | 310 | /** 311 | * Set the has one value 312 | * 313 | * @param string $relation 314 | * @param mixed $value 315 | * 316 | * @return \CanGelis\DataModels\DataModel 317 | */ 318 | abstract protected function setHasOne($relation, $value); 319 | 320 | /** 321 | * Set has many relation value 322 | * 323 | * @param string $relation 324 | * @param mixed $value 325 | * 326 | * @return \CanGelis\DataModels\DataCollection 327 | */ 328 | abstract protected function setHasMany($relation, $value); 329 | 330 | /** 331 | * Returns true if the attribute exists 332 | * 333 | * @param string $attribute 334 | * 335 | * @return bool 336 | */ 337 | abstract protected function hasAttribute($attribute); 338 | 339 | /** 340 | * Get the attribute value 341 | * 342 | * @param $attribute 343 | * 344 | * @return mixed 345 | */ 346 | abstract protected function getAttribute($attribute); 347 | 348 | /** 349 | * Called when an attribute is loaded 350 | * When the value is loaded it can be deleted from the 351 | * source data so no duplication will occur during export 352 | * 353 | * @param string $attribute 354 | * 355 | * @return mixed 356 | */ 357 | abstract protected function onLoadAttribute($attribute); 358 | } 359 | --------------------------------------------------------------------------------