Skip to content

Latest commit

 

History

History
674 lines (528 loc) · 15.1 KB

timestampable.md

File metadata and controls

674 lines (528 loc) · 15.1 KB

Timestampable behavior extension for Doctrine 2

Timestampable behavior will automate the update of date fields on your Entities or Documents. It works through annotations and can update fields on creation, update, property subset update, or even on specific property value change.

Features:

  • Automatic predefined date field update on creation, update, property subset update, and even on record property changes
  • ORM and ODM support using same listener
  • Specific annotations for properties, and no interface required
  • Can react to specific property or relation changes to specific value
  • Can be nested with other behaviors
  • Attribute, Annotation and Xml mapping support for extensions

This article will cover the basic installation and functionality of Timestampable behavior

Content:

Setup and autoloading

Read the documentation or check the example code on how to setup and use the extensions in most optimized way.

Timestampable Entity example:

Timestampable annotations:

  • @Gedmo\Mapping\Annotation\Timestampable this annotation tells that this column is timestampable. By default it updates this column on update. If column is not date, datetime or time type it will trigger an exception.

Timestampable attributes:

  • @Gedmo\Mapping\Annotation\Timestampable this attribute tells that this column is timestampable. By default it updates this column on update. If column is not date, datetime or time type it will trigger an exception.

Available configuration options:

  • on - is main option and can be create, update, change this tells when it should be updated
  • field - only valid if on="change" is specified, tracks property or a list of properties for changes
  • value - only valid if on="change" is specified and the tracked field is a single field (not an array), if the tracked field has this value

Note: that Timestampable interface is not necessary, except in cases where you need to identify entity as being Timestampable. The metadata is loaded only once then cache is activated

Annotations

<?php
namespace Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Article
{
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private $id;

    /**
     * @ORM\Column(type="string", length=128)
     */
    private $title;

    /**
     * @ORM\Column(name="body", type="string")
     */
    private $body;

    /**
     * @var \DateTime $created
     *
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(type="datetime")
     */
    private $created;

    /**
     * @var \DateTime $updated
     *
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(type="datetime")
     */
    private $updated;

    /**
     * @var \DateTime $contentChanged
     *
     * @ORM\Column(name="content_changed", type="datetime", nullable=true)
     * @Gedmo\Timestampable(on="change", field={"title", "body"})
     */
    private $contentChanged;

    public function getId()
    {
        return $this->id;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setBody($body)
    {
        $this->body = $body;
    }

    public function getBody()
    {
        return $this->body;
    }

    public function getCreated()
    {
        return $this->created;
    }

    public function getUpdated()
    {
        return $this->updated;
    }

    public function getContentChanged()
    {
        return $this->contentChanged;
    }
}

Attributes

#[ORM\Entity]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: Types::INTEGER)]
    private $id;

    #[ORM\Column(name: 'title', type: Types::STRING, length: 128)]
    private $title;

    #[ORM\Column(name: 'body', type: Types::STRING)]
    private $body;

    /**
     * @var \DateTime
     */
    #[Gedmo\Timestampable(on: 'create')]
    #[ORM\Column(name: 'created', type: Types::DATE_MUTABLE)]
    private $created;

    /**
     * @var \DateTime
     */
    #[ORM\Column(name: 'updated', type: Types::DATETIME_MUTABLE)]
    #[Gedmo\Timestampable]
    private $updated;

    /**
     * @var \DateTime
     */
    #[ORM\Column(name: 'content_changed', type: Types::DATETIME_MUTABLE, nullable: true)]
    #[Gedmo\Timestampable(on: 'change', field: ['title', 'body'])]
    private $contentChanged;

    public function getId()
    {
        return $this->id;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setBody($body)
    {
        $this->body = $body;
    }

    public function getBody()
    {
        return $this->body;
    }

    public function getCreated()
    {
        return $this->created;
    }

    public function getUpdated()
    {
        return $this->updated;
    }

    public function getContentChanged()
    {
        return $this->contentChanged;
    }
}

Timestampable Document example:

Note: this example is using annotations and attributes for mapping, you should use one of them, not both.

<?php
namespace Document;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
 * @ODM\Document(collection="articles")
 */
class Article
{
    /** @ODM\Id */
    private $id;

    /**
     * @ODM\Field(type="string")
     */
    private $title;

    /**
     * @ODM\Field(type="string")
     */
    private $body;

    /**
     * @var date $created
     *
     * @ODM\Date
     * @Gedmo\Timestampable(on="create")
     */
     #[Gedmo\Timestampable(on: 'create')]
    private $created;

    /**
     * @var date $updated
     *
     * @ODM\Date
     * @Gedmo\Timestampable
     */
     #[Gedmo\Timestampable]
    private $updated;

    /**
     * @var \DateTime $contentChanged
     *
     * @ODM\Date
     * @Gedmo\Timestampable(on="change", field={"title", "body"})
     */
     #[Gedmo\Timestampable(on: 'change', field: ['title', 'body'])]
    private $contentChanged;

    public function getId()
    {
        return $this->id;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setBody($body)
    {
        $this->body = $body;
    }

    public function getBody()
    {
        return $this->body;
    }

    public function getCreated()
    {
        return $this->created;
    }

    public function getUpdated()
    {
        return $this->updated;
    }

    public function getContentChanged()
    {
        return $this->contentChanged;
    }
}

Now on update and creation these annotated fields will be automatically updated

Xml mapping example

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">

    <entity name="Mapping\Fixture\Xml\Timestampable" table="timestampables">
        <id name="id" type="integer" column="id">
            <generator strategy="AUTO"/>
        </id>

        <field name="created" type="datetime">
            <gedmo:timestampable on="create"/>
        </field>
        <field name="updated" type="datetime">
            <gedmo:timestampable on="update"/>
        </field>
        <field name="published" type="datetime" nullable="true">
            <gedmo:timestampable on="change" field="status.title" value="Published"/>
        </field>

        <many-to-one field="status" target-entity="Status">
            <join-column name="status_id" referenced-column-name="id"/>
        </many-to-one>
    </entity>

</doctrine-mapping>

Advanced examples:

Using dependency of property changes

Add another entity which would represent Article Type:

<?php
namespace Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Type
{
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private $id;

    /**
     * @ORM\Column(type="string", length=128)
     */
    private $title;

    /**
     * @ORM\OneToMany(targetEntity="Article", mappedBy="type")
     */
    private $articles;

    public function getId()
    {
        return $this->id;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }
}

Now update the Article Entity to reflect published date on Type change:

<?php
namespace Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Article
{
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private $id;

    /**
     * @ORM\Column(type="string", length=128)
     */
    private $title;

    /**
     * @var \DateTime $created
     *
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(type="datetime")
     */
    private $created;

    /**
     * @var \DateTime $updated
     *
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(type="datetime")
     */
    private $updated;

    /**
     * @ORM\ManyToOne(targetEntity="Type", inversedBy="articles")
     */
    private $type;

    /**
     * @var \DateTime $published
     *
     * @ORM\Column(type="datetime", nullable=true)
     * @Gedmo\Timestampable(on="change", field="type.title", value="Published")
     *
     * or for example
     * @Gedmo\Timestampable(on="change", field="type.title", value={"Published", "Closed"})
     */
    private $published;

    public function setType($type)
    {
        $this->type = $type;
    }

    public function getId()
    {
        return $this->id;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getCreated()
    {
        return $this->created;
    }

    public function getUpdated()
    {
        return $this->updated;
    }

    public function getPublished()
    {
        return $this->published;
    }
}

Now few operations to get it all done:

<?php
$article = new Article;
$article->setTitle('My Article');

$em->persist($article);
$em->flush();
// article: $created, $updated were set

$type = new Type;
$type->setTitle('Published');

$article = $em->getRepository('Entity\Article')->findByTitle('My Article');
$article->setType($type);

$em->persist($article);
$em->persist($type);
$em->flush();
// article: $published, $updated were set

$article->getPublished()->format('Y-m-d'); // the date article type changed to published

Easy like that, any suggestions on improvements are very welcome

Creating a UTC DateTime type that stores your datetimes in UTC

First, we define our custom data type (note the type name is datetime and the type extends DateTimeType which simply overrides the default Doctrine type):

<?php

namespace Acme\DoctrineExtensions\DBAL\Types;

use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;

class UTCDateTimeType extends DateTimeType
{
    static private $utc = null;

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        if (is_null(self::$utc)) {
            self::$utc = new \DateTimeZone('UTC');
        }

        $value->setTimeZone(self::$utc);

        return $value->format($platform->getDateTimeFormatString());
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        if (is_null(self::$utc)) {
            self::$utc = new \DateTimeZone('UTC');
        }

        $val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, self::$utc);

        if (!$val) {
            throw ConversionException::conversionFailed($value, $this->getName());
        }

        return $val;
    }
}

Now in Symfony, we register and override the datetime type. WARNING: this will override the datetime type for all your entities and for all entities in external bundles or extensions, so if you have some entities that require the standard datetime type from Doctrine, you must modify the above type and use a different name (such as utcdatetime). Additionally, you'll need to modify Timestampable so that it includes utcdatetime as a valid type.

doctrine:
    dbal:
        types:
            datetime: Acme\DoctrineExtensions\DBAL\Types\UTCDateTimeType

And our Entity properties look as expected:

<?php
/**
 * @var \DateTime $dateCreated
 *
 * @ORM\Column(name="date_created", type="datetime")
 * @Gedmo\Timestampable(on="create")
 */
private $dateCreated;

/**
 * @var \DateTime $dateLastModified
 *
 * @Gedmo\Timestampable(on="update")
 * @ORM\Column(name="date_last_modified", type="datetime")
 */
private $dateLastModified;

Now, in our view (suppose we are using Symfony and Twig), we can display the datetime (which is persisted in UTC format) in our user's time zone:

{{ myEntity.dateCreated | date("d/m/Y g:i a", app.user.timezone) }}

Or if the user does not have a timezone, we could expand that to use a system/app/PHP default timezone.

This example is based off Handling different Timezones with the DateTime Type - however that example may be outdated because it contains some obviously invalid PHP from the TimeZone class.

Traits

You can use timestampable traits for quick createdAt updatedAt timestamp definitions when using annotation mapping. There is also a trait without annotations for easy integration purposes.

Note: this feature is only available since php 5.4.0. And you are not required to use the Traits provided by extensions.

<?php
namespace Timestampable\Fixture;

use Gedmo\Timestampable\Traits\TimestampableEntity;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class UsingTrait
{
    /**
     * Hook timestampable behavior
     * updates createdAt, updatedAt fields
     */
    use TimestampableEntity;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(length=128)
     */
    private $title;
}

Traits are very simple and if you use different field names I recommend to simply create your own ones based per project. These ones are standing as an example.