<?php
namespace CodeIgniter;
use CodeIgniter\Exceptions\EntityException;
use CodeIgniter\I18n\Time;
use CodeIgniter\Exceptions\CastException;
class Entity
{
protected $datamap = [];
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
];
protected $casts = [];
protected $attributes = [];
protected $original = [];
private $_cast = true;
public function __construct(array $data = null)
{
$this->syncOriginal();
$this->fill($data);
}
public function fill(array $data = null)
{
if (! is_array($data))
{
return $this;
}
foreach ($data as $key => $value)
{
$key = $this->mapProperty($key);
$method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
if (method_exists($this, $method))
{
$this->$method($value);
}
else
{
$this->attributes[$key] = $value;
}
}
return $this;
}
public function toArray(bool $onlyChanged = false, bool $cast = true): array
{
$this->_cast = $cast;
$return = [];
foreach ($this->attributes as $key => $value)
{
if (substr($key, 0, 1) === '_')
{
continue;
}
if ($onlyChanged && ! $this->hasChanged($key))
{
continue;
}
$return[$key] = $this->__get($key);
}
if (is_array($this->datamap))
{
foreach ($this->datamap as $from => $to)
{
if (array_key_exists($to, $return))
{
$return[$from] = $this->__get($to);
}
}
}
$this->_cast = true;
return $return;
}
public function toRawArray(bool $onlyChanged = false): array
{
$return = [];
if (! $onlyChanged)
{
return $this->attributes;
}
foreach ($this->attributes as $key => $value)
{
if (! $this->hasChanged($key))
{
continue;
}
$return[$key] = $this->attributes[$key];
}
return $return;
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
public function hasChanged(string $key = null): bool
{
if ($key === null)
{
return $this->original !== $this->attributes;
}
if (! array_key_exists($key, $this->original) && ! array_key_exists($key, $this->attributes))
{
return false;
}
if (! array_key_exists($key, $this->original) && array_key_exists($key, $this->attributes))
{
return true;
}
return $this->original[$key] !== $this->attributes[$key];
}
public function __get(string $key)
{
$key = $this->mapProperty($key);
$result = null;
$method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
if (method_exists($this, $method))
{
$result = $this->$method();
}
else if (array_key_exists($key, $this->attributes))
{
$result = $this->attributes[$key];
}
if (in_array($key, $this->dates))
{
$result = $this->mutateDate($result);
}
else if ($this->_cast && isset($this->casts[$key]) && ! empty($this->casts[$key]))
{
$result = $this->castAs($result, $this->casts[$key]);
}
return $result;
}
public function __set(string $key, $value = null)
{
$key = $this->mapProperty($key);
if (in_array($key, $this->dates))
{
$value = $this->mutateDate($value);
}
$isNullable = false;
$castTo = false;
if (array_key_exists($key, $this->casts))
{
$isNullable = substr($this->casts[$key], 0, 1) === '?';
$castTo = $isNullable ? substr($this->casts[$key], 1) : $this->casts[$key];
}
if (! $isNullable || ! is_null($value))
{
if ($castTo === 'array')
{
$value = serialize($value);
}
if (($castTo === 'json' || $castTo === 'json-array') && function_exists('json_encode'))
{
$value = json_encode($value);
if (json_last_error() !== JSON_ERROR_NONE)
{
throw CastException::forInvalidJsonFormatException(json_last_error());
}
}
}
$method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
if (method_exists($this, $method))
{
$this->$method($value);
return $this;
}
$this->attributes[$key] = $value;
return $this;
}
public function __unset(string $key)
{
unset($this->attributes[$key]);
}
public function __isset(string $key): bool
{
$key = $this->mapProperty($key);
$method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
if (method_exists($this, $method))
{
return true;
}
return isset($this->attributes[$key]);
}
public function setAttributes(array $data)
{
$this->attributes = $data;
$this->syncOriginal();
return $this;
}
protected function mapProperty(string $key)
{
if (empty($this->datamap))
{
return $key;
}
if (isset($this->datamap[$key]) && ! empty($this->datamap[$key]))
{
return $this->datamap[$key];
}
return $key;
}
protected function mutateDate($value)
{
if ($value instanceof Time)
{
return $value;
}
if ($value instanceof \DateTime)
{
return Time::instance($value);
}
if (is_numeric($value))
{
return Time::createFromTimestamp($value);
}
if (is_string($value))
{
return Time::parse($value);
}
return $value;
}
protected function castAs($value, string $type)
{
if (substr($type, 0, 1) === '?')
{
if ($value === null)
{
return null;
}
$type = substr($type, 1);
}
switch($type)
{
case 'int':
case 'integer': $value = (int)$value;
break;
case 'float':
$value = (float)$value;
break;
case 'double':
$value = (double)$value;
break;
case 'string':
$value = (string)$value;
break;
case 'bool':
case 'boolean': $value = (bool)$value;
break;
case 'object':
$value = (object)$value;
break;
case 'array':
if (is_string($value) && (strpos($value, 'a:') === 0 || strpos($value, 's:') === 0))
{
$value = unserialize($value);
}
$value = (array)$value;
break;
case 'json':
$value = $this->castAsJson($value, false);
break;
case 'json-array':
$value = $this->castAsJson($value, true);
break;
case 'datetime':
return new \DateTime($value);
break;
case 'timestamp':
return strtotime($value);
break;
}
return $value;
}
private function castAsJson($value, bool $asArray = false)
{
$tmp = ! is_null($value) ? ($asArray ? [] : new \stdClass) : null;
if (function_exists('json_decode'))
{
if ((is_string($value) && strlen($value) > 1 && in_array($value{0}, ['[', '{', '"'])) || is_numeric($value))
{
$tmp = json_decode($value, $asArray);
if (json_last_error() !== JSON_ERROR_NONE)
{
throw CastException::forInvalidJsonFormatException(json_last_error());
}
}
}
return $tmp;
}
}