Skip to content

Commit

Permalink
Refactor Entity so that more accurate changed data can be determined.…
Browse files Browse the repository at this point in the history
… Also brings better usability. Fixes #1992
  • Loading branch information
lonnieezell committed May 13, 2019
1 parent e0f2169 commit 0ca7b5e
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 252 deletions.
5 changes: 5 additions & 0 deletions system/Database/BaseResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ public function getCustomResultObject(string $className)

while ($row = $this->fetchObject($className))
{
if (method_exists($row, 'syncOriginal'))
{
$row->syncOriginal();
}

$this->customResultObject[$className][] = $row;
}

Expand Down
210 changes: 96 additions & 114 deletions system/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,43 +47,45 @@
*/
class Entity
{
/**
* Maps names used in sets and gets against unique
* names within the class, allowing independence from
* database column names.
*
* Example:
* $datamap = [
* 'db_name' => 'class_name'
* ];
*/
protected $_options = [
'datamap' => [],

/**
* Define properties that are automatically converted to Time instances.
*/
'dates' => [
'created_at',
'updated_at',
'deleted_at',
],

/**
* Array of field names and the type of value to cast them as
* when they are accessed.
*/
'casts' => [],
/**
* Maps names used in sets and gets against unique
* names within the class, allowing independence from
* database column names.
*
* Example:
* $datamap = [
* 'db_name' => 'class_name'
* ];
*/
protected $datamap = [];

protected $dates = [
'created_at',
'updated_at',
'deleted_at',
];

/**
* Array of field names and the type of value to cast them as
* when they are accessed.
*/
protected $casts = [];

/**
* Holds the current values of all class vars.
*
* @var array
*/
protected $attributes = [];

/**
* 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 = [];
protected $original = [];

/**
* Holds info whenever properties have to be casted
Expand All @@ -99,24 +101,9 @@ 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);
$this->syncOriginal();

foreach ($properties as $key => $value)
{
if (substr($key, 0, 1) === '_')
{
unset($properties[$key]);
}
}

$this->_original = $properties;

if (is_array($data))
{
$this->fill($data);
}
$this->fill($data);
}

/**
Expand All @@ -128,8 +115,13 @@ public function __construct(array $data = null)
*
* @return \CodeIgniter\Entity
*/
public function fill(array $data)
public function fill(array $data = null)
{
if (! is_array($data))
{
return $this;
}

foreach ($data as $key => $value)
{
$key = $this->mapProperty($key);
Expand All @@ -140,9 +132,9 @@ public function fill(array $data)
{
$this->$method($value);
}
elseif (property_exists($this, $key))
else
{
$this->$key = $value;
$this->attributes[$key] = $value;
}
}

Expand Down Expand Up @@ -170,16 +162,14 @@ public function toArray(bool $onlyChanged = false, bool $cast = true): array

// we need to loop over our properties so that we
// allow our magic methods a chance to do their thing.
$properties = get_object_vars($this);

foreach ($properties as $key => $value)
foreach ($this->attributes as $key => $value)
{
if (substr($key, 0, 1) === '_')
{
continue;
}

if ($onlyChanged && ! $this->hasPropertyChanged($key, $value))
if ($onlyChanged && ! $this->hasChanged($key))
{
continue;
}
Expand All @@ -188,9 +178,9 @@ public function toArray(bool $onlyChanged = false, bool $cast = true): array
}

// Loop over our mapped properties and add them to the list...
if (is_array($this->_options['datamap']))
if (is_array($this->datamap))
{
foreach ($this->_options['datamap'] as $from => $to)
foreach ($this->datamap as $from => $to)
{
$return[$from] = $this->__get($to);
}
Expand All @@ -202,9 +192,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true): array
//--------------------------------------------------------------------

/**
* Converts the properties of this class into an array. Unlike toArray()
* this will not cast the data or use any magic accessors. It simply
* returns the raw data for use when saving to the model, etc.
* Returns the raw values of the current attributes.
*
* @param boolean $onlyChanged
*
Expand All @@ -214,39 +202,60 @@ public function toRawArray(bool $onlyChanged = false): array
{
$return = [];

$properties = get_object_vars($this);

foreach ($properties as $key => $value)
if (! $onlyChanged)
{
if (substr($key, 0, 1) === '_')
{
continue;
}
return $this->attributes;
}

if ($onlyChanged && ! $this->hasPropertyChanged($key, $value))
foreach ($this->attributes as $key => $value)
{
if (! $this->hasChanged($key))
{
continue;
}

$return[$key] = $this->$key;
$return[$key] = $this->attributes[$key];
}

return $return;
}

//--------------------------------------------------------------------

/**
* Ensures our "original" values match the current values.
*
* @return $this
*/
public function syncOriginal()
{
$this->original = $this->attributes;

return $this;
}

/**
* Checks a property to see if it has changed since the entity was created.
*
* @param string $key
* @param null $value
*
* @return boolean
*/
protected function hasPropertyChanged(string $key, $value = null): bool
public function hasChanged(string $key): bool
{
return ! (($this->_original[$key] === null && $value === null) || $this->_original[$key] === $value);
// Key doesn't exist in either
if (! array_key_exists($key, $this->original) && ! array_key_exists($key, $this->attributes))
{
return false;
}

// It's a new element
if (! array_key_exists($key, $this->original) && array_key_exists($key, $this->attributes))
{
return true;
}

return $this->original[$key] !== $this->attributes[$key];
}

/**
Expand All @@ -266,7 +275,8 @@ protected function hasPropertyChanged(string $key, $value = null): bool
*/
public function __get(string $key)
{
$key = $this->mapProperty($key);
$key = $this->mapProperty($key);
$result = null;

// Convert to CamelCase for the method
$method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
Expand All @@ -280,25 +290,20 @@ public function __get(string $key)

// Otherwise return the protected property
// if it exists.
else if (property_exists($this, $key))
else if (array_key_exists($key, $this->attributes))
{
$result = $this->$key;
$result = $this->attributes[$key];
}

// Do we need to mutate this into a date?
if (in_array($key, $this->_options['dates']))
if (in_array($key, $this->dates))
{
$result = $this->mutateDate($result);
}
// Or cast it as something?
else if ($this->_cast && isset($this->_options['casts'][$key]) && ! empty($this->_options['casts'][$key]))
else if ($this->_cast && isset($this->casts[$key]) && ! empty($this->casts[$key]))
{
$result = $this->castAs($result, $this->_options['casts'][$key]);
}

if (! isset($result) && ! property_exists($this, $key))
{
throw EntityException::forTryingToAccessNonExistentProperty($key, get_called_class());
$result = $this->castAs($result, $this->casts[$key]);
}

return $result;
Expand Down Expand Up @@ -326,18 +331,18 @@ public function __set(string $key, $value = null)
$key = $this->mapProperty($key);

// Check if the field should be mutated into a date
if (in_array($key, $this->_options['dates']))
if (in_array($key, $this->dates))
{
$value = $this->mutateDate($value);
}

$isNullable = false;
$castTo = false;

if (array_key_exists($key, $this->_options['casts']))
if (array_key_exists($key, $this->casts))
{
$isNullable = substr($this->_options['casts'][$key], 0, 1) === '?';
$castTo = $isNullable ? substr($this->_options['casts'][$key], 1) : $this->_options['casts'][$key];
$isNullable = substr($this->casts[$key], 0, 1) === '?';
$castTo = $isNullable ? substr($this->casts[$key], 1) : $this->casts[$key];
}

if (! $isNullable || ! is_null($value))
Expand Down Expand Up @@ -381,42 +386,23 @@ public function __set(string $key, $value = null)
// they cannot be saved. Useful for
// grabbing values through joins,
// assigning relationships, etc.
$this->$key = $value;
$this->attributes[$key] = $value;

return $this;
}

//--------------------------------------------------------------------

/**
* Unsets a protected/private class property. Sets the value to null.
* However, if there was a default value for the parent class, this
* attribute will be reset to that default value.
* Unsets an attribute property.
*
* @param string $key
*
* @throws \ReflectionException
*/
public function __unset(string $key)
{
// If not actual property exists, get out
// before we confuse our data mapping.
if (! property_exists($this, $key))
{
return;
}

$this->$key = null;

// Get the class' original default value for this property
// so we can reset it to the original value.
$reflectionClass = new \ReflectionClass($this);
$defaultProperties = $reflectionClass->getDefaultProperties();

if (isset($defaultProperties[$key]))
{
$this->$key = $defaultProperties[$key];
}
unset($this->attributes[$key]);
}

//--------------------------------------------------------------------
Expand All @@ -431,11 +417,7 @@ public function __unset(string $key)
*/
public function __isset(string $key): bool
{
// Ensure an actual property exists, otherwise
// we confuse the data mapping.
$value = property_exists($this, $key) ? $this->$key : null;

return ! is_null($value);
return isset($this->attributes[$key]);
}

//--------------------------------------------------------------------
Expand All @@ -450,14 +432,14 @@ public function __isset(string $key): bool
*/
protected function mapProperty(string $key)
{
if (empty($this->_options['datamap']))
if (empty($this->datamap))
{
return $key;
}

if (isset($this->_options['datamap'][$key]) && ! empty($this->_options['datamap'][$key]))
if (isset($this->datamap[$key]) && ! empty($this->datamap[$key]))
{
return $this->_options['datamap'][$key];
return $this->datamap[$key];
}

return $key;
Expand Down
Loading

1 comment on commit 0ca7b5e

@MGatner
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that is a lot changed! I’m excited to check this out when I’m off mobile.

Please sign in to comment.