diff --git a/system/Entity.php b/system/Entity.php index 2f4e4994fac7..c489d66df0e7 100644 --- a/system/Entity.php +++ b/system/Entity.php @@ -64,6 +64,15 @@ class Entity 'casts' => [] ]; + /** + * Holds original copies of all class vars so + * we can determine what's actually been changed + * and not accidentally write nulls where we shouldn't. + * + * @var array + */ + protected $_original = []; + /** * Allows filling in Entity parameters during construction. * @@ -71,6 +80,20 @@ class Entity */ public function __construct(array $data = null) { + // Collect any original values of things + // so we can compare later to see what's changed + $properties = get_object_vars($this); + + foreach ($properties as $key => $value) + { + if (substr($key, 0, 1) == '_') + { + unset($properties[$key]); + } + } + + $this->_original = $properties; + if (is_array($data)) { $this->fill($data); @@ -108,8 +131,12 @@ public function fill(array $data) * values of this entity as an array. All values are accessed * through the __get() magic method so will have any casts, etc * applied to them. + * + * @param bool $onlyChanged If true, only return values that have changed since object creation + * + * @return array */ - public function toArray(): array + public function toArray(bool $onlyChanged = false): array { $return = []; @@ -119,7 +146,12 @@ public function toArray(): array foreach ($properties as $key => $value) { - if ($key == '_options') continue; + if (substr($key, 0, 1) == '_') continue; + + if ($onlyChanged && $this->_original[$key] === null && $value === null) + { + continue; + } $return[$key] = $this->__get($key); } @@ -368,7 +400,7 @@ protected function mutateDate($value) * * @return mixed */ - + protected function castAs($value, string $type) { switch($type) @@ -422,7 +454,7 @@ protected function castAs($value, string $type) * Cast as JSON * * @param mixed $value - * @param bool $asArray + * @param bool $asArray * * @return mixed */ diff --git a/system/Model.php b/system/Model.php index aa9d0aeda22a..5b413ad79ad7 100644 --- a/system/Model.php +++ b/system/Model.php @@ -479,45 +479,59 @@ public function save($data) * properties as an array suitable for use in creates and updates. * * @param string|object $data - * @param string $dateFormat + * @param string $dateFormat * * @return array + * @throws \ReflectionException */ public static function classToArray($data, string $dateFormat = 'datetime'): array { - $mirror = new \ReflectionClass($data); - $props = $mirror->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED); - - $properties = []; - - // Loop over each property, - // saving the name/value in a new array we can return. - foreach ($props as $prop) + if (method_exists($data, 'toArray')) + { + $properties = $data->toArray(true); + } + else { - // Must make protected values accessible. - $prop->setAccessible(true); - $propName = $prop->getName(); - $properties[$propName] = $prop->getValue($data); + $mirror = new \ReflectionClass($data); + $props = $mirror->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED); + + $properties = []; - // Convert any Time instances to appropriate $dateFormat - if ($properties[$propName] instanceof Time) + // Loop over each property, + // saving the name/value in a new array we can return. + foreach ($props as $prop) { - $converted = (string)$properties[$propName]; + // Must make protected values accessible. + $prop->setAccessible(true); + $propName = $prop->getName(); + $properties[$propName] = $prop->getValue($data); + } + } - switch($dateFormat) + // Convert any Time instances to appropriate $dateFormat + if (count($properties)) + { + foreach ($properties as $key => $value) + { + if ($value instanceof Time) { - case 'datetime': - $converted = $properties[$propName]->format('Y-m-d H:i:s'); - break; - case 'date': - $converted = $properties[$propName]->format('Y-m-d'); - break; - case 'int': - $converted = $properties[$propName]->getTimestamp(); - break; - } + switch ($dateFormat) + { + case 'datetime': + $converted = $value->format('Y-m-d H:i:s'); + break; + case 'date': + $converted = $value->format('Y-m-d'); + break; + case 'int': + $converted = $value->getTimestamp(); + break; + default: + $converted = (string)$value; + } - $properties[$prop->getName()] = $converted; + $properties[$key] = $converted; + } } } @@ -1077,11 +1091,14 @@ protected function doProtectFields($data) throw DataException::forInvalidAllowedFields(get_class($this)); } - foreach ($data as $key => $val) + if (is_array($data) && count($data)) { - if ( ! in_array($key, $this->allowedFields)) + foreach ($data as $key => $val) { - unset($data[$key]); + if (! in_array($key, $this->allowedFields)) + { + unset($data[$key]); + } } } diff --git a/tests/_support/Models/SimpleEntity.php b/tests/_support/Models/SimpleEntity.php index 2664fb7294f1..31d29a4b99c8 100644 --- a/tests/_support/Models/SimpleEntity.php +++ b/tests/_support/Models/SimpleEntity.php @@ -17,4 +17,9 @@ class SimpleEntity extends Entity protected $description; protected $created_at; + protected $_options = [ + 'datamap' => [], + 'dates' => ['created_at', 'updated_at', 'deleted_at'], + 'casts' => [] + ]; } diff --git a/tests/system/Database/Live/ModelTest.php b/tests/system/Database/Live/ModelTest.php index baa8387e8484..221911d7e777 100644 --- a/tests/system/Database/Live/ModelTest.php +++ b/tests/system/Database/Live/ModelTest.php @@ -712,4 +712,28 @@ public function testUpdateBatchValidationFail() } //-------------------------------------------------------------------- + + public function testSelectAndEntitiesSaveOnlyChangedValues() + { + $this->hasInDatabase('job', [ + 'name' => 'Rocket Scientist', + 'description' => 'Plays guitar for Queen', + 'created_at' => date('Y-m-d H:i:s') + ]); + + $model = new EntityModel(); + + $job = $model->select('id, name')->where('name', 'Rocket Scientist')->first(); + + $this->assertNull($job->description); + $this->assertEquals('Rocket Scientist', $job->name); + + $model->save($job); + + $this->seeInDatabase('job', [ + 'id' => $job->id, + 'name' => 'Rocket Scientist', + 'description' => 'Plays guitar for Queen', + ]); + } } diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index 3ed3958aee10..24bba776d7ee 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -213,7 +213,7 @@ public function testParseLoopEntityProperties() $power = new class extends \CodeIgniter\Entity { public $foo = 'bar'; protected $bar = 'baz'; - public function toArray(): array + public function toArray(bool $onlyChanged = false): array { return [ 'foo' => $this->foo,