diff --git a/README.md b/README.md index c200533..b67ffe6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ class User extends Model { protected $casts = ['age' => 'integer']; + protected $dates = ['added_at']; + public function save() { return API::post('/items', $this->attributes); diff --git a/src/Model.php b/src/Model.php index cfc73a0..60cdb3e 100644 --- a/src/Model.php +++ b/src/Model.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Arr; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Illuminate\Support\Collection as BaseCollection; use JsonSerializable; @@ -60,6 +61,20 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab */ protected $casts = []; + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = []; + + /** + * The storage format of the model's date columns. + * + * @var string + */ + protected $dateFormat = 'Y-m-d H:i:s'; + /** * Indicates whether attributes are snake cased on arrays. * @@ -480,6 +495,18 @@ public function attributesToArray() ); } + $dateAttributes = $this->dates; + + foreach ($dateAttributes as $attribute) { + if (! array_key_exists($attribute, $attributes) || + in_array($attribute, $mutatedAttributes) || + ! $attributes[$attribute]) { + continue; + } + + $attributes[$attribute] = $this->asDateTime($attributes[$attribute]); + } + // Next we will handle any casts that have been setup for this model and cast // the values to their appropriate type. If the attribute has a mutator we // will not perform the cast on those attributes to avoid any confusion. @@ -580,6 +607,14 @@ protected function getAttributeValue($key) $value = $this->castAttribute($key, $value); } + // If the attribute is listed as a date, we will convert it to a DateTime + // instance on retrieval, which makes it quite convenient to work with + // date fields without having to create a mutator for each property. + if (in_array($key, $this->dates) && + ! is_null($value)) { + return $this->asDateTime($value); + } + return $value; } @@ -701,11 +736,97 @@ protected function castAttribute($key, $value) return $this->fromJson($value); case 'collection': return new BaseCollection($this->fromJson($value)); + case 'date': + return $this->asDate($value); + case 'datetime': + case 'custom_datetime': + return $this->asDateTime($value); + case 'timestamp': + return $this->asTimestamp($value); default: return $value; } } + /** + * Return a timestamp as DateTime object with time set to 00:00:00. + * + * @param mixed $value + * @return \Illuminate\Support\Carbon + */ + protected function asDate($value) + { + return $this->asDateTime($value)->startOfDay(); + } + + /** + * Return a timestamp as DateTime object. + * + * @param mixed $value + * @return \Illuminate\Support\Carbon + */ + protected function asDateTime($value) + { + // If this value is already a Carbon instance, we shall just return it as is. + // This prevents us having to re-instantiate a Carbon instance when we know + // it already is one, which wouldn't be fulfilled by the DateTime check. + if ($value instanceof Carbon) { + return $value; + } + + // If the value is already a DateTime instance, we will just skip the rest of + // these checks since they will be a waste of time, and hinder performance + // when checking the field. We will just return the DateTime right away. + if ($value instanceof DateTimeInterface) { + return new Carbon( + $value->format('Y-m-d H:i:s.u'), $value->getTimezone() + ); + } + + // If this value is an integer, we will assume it is a UNIX timestamp's value + // and format a Carbon object from this timestamp. This allows flexibility + // when defining your date fields as they might be UNIX timestamps here. + if (is_numeric($value)) { + return Carbon::createFromTimestamp($value); + } + + // If the value is in simply year, month, day format, we will instantiate the + // Carbon instances from that format. Again, this provides for simple date + // fields on the database, while still supporting Carbonized conversion. + if ($this->isStandardDateFormat($value)) { + return Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); + } + + // Finally, we will just assume this date is in the format used by default on + // the database connection and use that format to create the Carbon object + // that is returned back out to the developers after we convert it here. + return Carbon::createFromFormat( + str_replace('.v', '.u', $this->dateFormat), $value + ); + } + + /** + * Determine if the given value is a standard date format. + * + * @param string $value + * @return bool + */ + protected function isStandardDateFormat($value) + { + return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value); + } + + /** + * Return a timestamp as unix timestamp. + * + * @param mixed $value + * @return int + */ + protected function asTimestamp($value) + { + return $this->asDateTime($value)->getTimestamp(); + } + /** * Set a given attribute on the model. * diff --git a/tests/ModelTest.php b/tests/ModelTest.php index eb063f9..7494c3a 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -171,6 +171,10 @@ public function testCasts() $model->active = 'true'; $model->default = 'bar'; $model->collection_data = [['foo' => 'bar', 'baz' => 'bat']]; + $model->created_at = '2018-10-12 03:45:22'; + $model->updated_at = '2018-10-12 03:45:22'; + $model->deleted_at = '2018-10-12 03:45:22'; + $model->added_at = '2018-10-12 03:45:22'; $this->assertTrue(is_float($model->score)); $this->assertTrue(is_array($model->data)); @@ -179,6 +183,10 @@ public function testCasts() $this->assertEquals('bar', $model->default); $this->assertInstanceOf('\stdClass', $model->object_data); $this->assertInstanceOf('\Illuminate\Support\Collection', $model->collection_data); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $model->created_at); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $model->updated_at); + $this->assertTrue(is_int($model->deleted_at)); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $model->added_at); $attributes = $model->getAttributes(); $this->assertTrue(is_string($attributes['score'])); @@ -188,6 +196,10 @@ public function testCasts() $this->assertTrue(is_string($attributes['default'])); $this->assertTrue(is_string($attributes['object_data'])); $this->assertTrue(is_string($attributes['collection_data'])); + $this->assertTrue(is_string($attributes['created_at'])); + $this->assertTrue(is_string($attributes['updated_at'])); + $this->assertTrue(is_string($attributes['deleted_at'])); + $this->assertTrue(is_string($attributes['added_at'])); $array = $model->toArray(); $this->assertTrue(is_float($array['score'])); @@ -197,6 +209,10 @@ public function testCasts() $this->assertEquals('bar', $array['default']); $this->assertInstanceOf('\stdClass', $array['object_data']); $this->assertInstanceOf('\Illuminate\Support\Collection', $array['collection_data']); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $array['created_at']); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $array['updated_at']); + $this->assertTrue(is_int($model->deleted_at)); + $this->assertInstanceOf('\Illuminate\Support\Carbon', $array['added_at']); } public function testGuarded() diff --git a/tests/stubs/ModelStub.php b/tests/stubs/ModelStub.php index a543c89..630f536 100644 --- a/tests/stubs/ModelStub.php +++ b/tests/stubs/ModelStub.php @@ -15,9 +15,16 @@ class ModelStub extends Model 'count' => 'int', 'object_data' => 'object', 'collection_data' => 'collection', + 'created_at' => 'date', + 'updated_at' => 'datetime', + 'deleted_at' => 'timestamp', 'foo' => 'bar', ]; + protected $dates = [ + 'added_at', + ]; + protected $guarded = [ 'secret', ]; @@ -33,6 +40,10 @@ class ModelStub extends Model 'object_data', 'default', 'collection_data', + 'created_at', + 'updated_at', + 'deleted_at', + 'added_at', ]; public function getListItemsAttribute($value)