yiisoft / active-record

Active Record database abstraction layer
https://www.yiiframework.com/
BSD 3-Clause "New" or "Revised" License
70 stars 28 forks source link

BaseActiveRecordTrait Date Mutators for dates columns #96

Open mohavee opened 4 years ago

mohavee commented 4 years ago

Feature request to add the ability to convert dates columns to instances of DateTime inspired by [laravel].(https://laravel.com/docs/7.x/eloquent-mutators#date-mutators).

It would be useful to work with dates column like that:

class Order extends ActiveRecord
{
    use MutateDatesAttributes;

    protected $dates = [
        'is_send_scheduled', 'created_at', 'updated_at'
    ];
}

$order->is_send_scheduled = (new \DateTime)->add(new DateInterval('PT4H'))

I implemented in trait that logic on my own like that:

trait MutateDatesAttributes
{
    /**
     * 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;

    /**
     * @param $name
     * @return DateTime
     * @throws \Exception
     */
    public function __get($name)
    {
        $value = parent::__get($name);

        // 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 ($this->isDateAttribute($name) &&
            ! is_null($value)) {
            return $this->asDateTime($value);
        }

        return $value;
    }

    /**
     * @param $name
     * @return string
     * @throws \Exception
     */
    public function getAttribute($name)
    {
        $value = parent::getAttribute($name);

        // 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 ($this->isDateAttribute($name) &&
            ! is_null($value)) {
            return $this->asDateTime($value);
        }

        return $value;
    }

    /**
     * Return a timestamp as DateTime object.
     *
     * @param mixed $value
     * @return DateTime
     * @throws \Exception
     */
    protected function asDateTime($value)
    {
        // If this value is already a DateTime instance, we shall just return it as is.
        // This prevents us having to re-instantiate a DateTime instance when we know
        // it already is one, which wouldn't be fulfilled by the DateTime check.
        if ($value instanceof DateTime) {
            return $value;
        }

        // If this value is an integer, we will assume it is a UNIX timestamp's value
        // and format a \DateTime object from this timestamp. This allows flexibility
        // when defining your date fields as they might be UNIX timestamps here.
        if (is_numeric($value)) {
            return (new DateTime)->setTimestamp($value);
        }

        // If the value is in simply year, month, day format, we will instantiate the
        // DateTime instances from that format. Again, this provides for simple date
        // fields on the database, while still supporting DateTime conversion.
        if ($this->isStandardDateFormat($value)) {
            return DateTime::createFromFormat('Y-m-d', $value)->setTime(0, 0);
        }

        // 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 DateTime object
        // that is returned back out to the developers after we convert it here.
        return DateTime::createFromFormat(
            str_replace('.v', '.u', $this->getDateFormat()), $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);
    }

    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    public function getDateFormat()
    {
        /** @var Connection $db */
        $db = static::getDb();
        switch ($db->getDriverName()) {
            case 'mysql':
            default:
                return $this->dateFormat ?: 'Y-m-d H:i:s';
        }
    }

    /**
     * Determine if the given attribute is a date.
     *
     * @param  string  $name
     * @return bool
     */
    protected function isDateAttribute($name)
    {
        return in_array($name, $this->dates);
    }

    /**
     * @param $name
     * @param $value
     * @throws \Exception
     */
    public function __set($name, $value)
    {
        if ($this->isDateAttribute($name)) {
            $value = $this->fromDateTime($value);
        }

        return parent::__set($name, $value);
    }

    /**
     * @param $name
     * @param $value
     * @throws \Exception
     */
    public function setAttribute($name, $value)
    {
        if ($this->isDateAttribute($name)) {
            $value = $this->fromDateTime($value);
        }

        return parent::setAttribute($name, $value);
    }

    /**
     * Convert a DateTime to a storable string.
     *
     * @param mixed $value
     * @return string|null
     * @throws \Exception
     */
    public function fromDateTime($value)
    {
        return empty($value) ? $value : $this->asDateTime($value)->format(
            $this->getDateFormat()
        );
    }
}

If that feature is ok i would have create pr.

samdark commented 4 years ago

Data mapper overall is a good idea.

Tigrov commented 6 months ago

Can be done inside db package by typecasting