PHP PDO Fetch MySQL DateTime

Viewed 603

I have a PHP class that represent MySQL table. One of that column table type is DateTime. Previously I use string and everything work fine, because I don't have to deal with the date type. I just use fetchAll function and the column table automatically mapping to a propriate field.

$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_CLASS, MyPHPClass::class);

Now I want to use the DateTime type in my PHP script. Is this possible to automatically convert MySQL DateTime to PHP DateTime when use PDO fetchAll? If yes, how?

Note: I know how to convert the DateTime string from MySQL to PHP DateTime, I just wonder if this is possible to add something like @Annotation, or converter.

3 Answers

We can take advantage of the fact that PHP will call __set magic method for all undefined properties so we can initialize our DateTime object there.

class User {
    public string $name;
    public DateTime $dateObject;
    
    public function __set($property, $value) {
        if ($property === 'date') {
            $this->dateObject = new DateTime($value);
        } else {
            $this->$property = $value;
        }
    }
}

$stmt->fetchAll(PDO::FETCH_CLASS, User::class);

Note: the column name in the database must differ from the property name in the User object, otherwise __set method will not be called.

For this purpose the concept of so called hydrators is very common. Especially for data fields of the type DateTime, which will most likely be repeated in other models, it makes sense to use hydrators. This keeps the logic away from the models and works with reusable code.

Why Hydrators?

If you are considering using your entire development with another database system, or if you simply want to maintain the greatest possible flexibility with your data models, hydrators make perfect sense. As mentioned earlier, hydrators can ensure that the models remain free of any logic. In addition, hydrators can be used to represent flexible scenarios. In addition, the hydration of data solely on the basis of the possibilities offered by the PHP PDO class is very weak. Just handle the raw data from the database as array and let the hydrator do the magic.

The Logic Behind Hydrators

Each hydrator can apply different strategies to the properties of the object to be hydrated. These hydrator strategies can be used to change values or perform other functions in the model before the actual hydration.

<?php
declare(strict_types=1);
namespace Marcel\Hydrator;

interface HydratorInterface
{
    public function hydrate(array $data, object $model): object;

    public function extract(object $model): array;
}

The above shown interface should be implemented in every hydrator class. Every hydrator should have a hydrate method, which pushes a given array of data into a given model. Furthermore there has to be the turnaround which is the extract method, which extracts data out of an model into an array.

<?php
declare(strict_types=1);
namespace Marcel\Hydrator\Strategy;

interface StrategyInterface 
{
    public function hydrate($value);
}

Both interfaces define the methods that hydrators and hydrator strategies must bring. These interfaces are mainly used to achieve secure type hinting for the identification of objects.

The Hydrator Strategy

<?php
declare(strict_types=1);
namespace Marcel\Hydrator\Strategy;

use DateTime;

class DateTimeStrategy implements StrategyInterface
{
    public function hydrate($value) 
    {
        $value = new DateTime($value);
        return $value;
    }
}

This simple example of an hydrator strategy does nothing more than taking the original value and initializing a new DateTime object with it. For the sake of simple illustration, I have omitted the error handling here. In production, you should always check at this point whether the DateTime object was really created and did not generate any errors.

The Hydrator

<?php
declare(strict_types=1);
namespace Marcel\Hydrator;

use Marcel\Hydrator\Strategy\StrategyInterface;
use ReflectionClass;

class ClassMethodsHydrator implements HydratorInterface
{
    protected ?ReflectionClass $reflector = null;

    protected array $strategies = [];

    public function hydrate(array $data, object $model): object
    {
        if ($this->reflector === null) {
            $this->reflector = new ReflectionClass($model);
        }

        foreach ($data as $key => $value) {
            if ($this->hasStrategy($key)) {
                $strategy = $this->strategies[$key];
                $value = $strategy->hydrate($value);
            }

            $methodName = 'set' . ucfirst($key);
            if ($this->reflector->hasMethod($methodName)) {
                $model->{$methodName}($value);
            }
        }

        return $model;
    }

    public function extract(object $model): array
    {
        return get_object_vars($model);
    }

    public function addStrategy(string $name, StrategyInterface $strategy): void
    {
        $this->strategies[$name] = $strategy; 
    }
    
    public function hasStrategy(string $name): bool
    {
        return array_key_exists($name, $this->strategies);
    }
}

This hydrator requires that your models have getter and setter methods. In this example, it requires at least that there is a corresponding setter method for each property. To avoid errors and to name methods correctly, the names of the column names should be filtered from the database. Normally, the names in the database are noted with an underscore and the properties of a model follow the camel case convention. (Example: "fancy_string" => "setFancyString")

The Example

class User 
{
    protected int $id;

    protected DateTime $birthday;

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

    public function setId(int $id): void
    {
        $this->id = $id;
    }

    public function getBirtday(): DateTime
    {
        return $this->birthday;
    }

    public function setBirthday(DateTime $birthday): void
    {
        $this->birthday = $birthday;
    }
}

$data = [
    'id' => 1,
    'birthday' => '1979-12-19',
];

$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('birthday', new DateTimeStrategy());
$user = $hydrator->hydrate($data, new User());   

The result of this code will be a fine hydrated user model.

object(Marcel\Model\User)#3 (2) {
  ["id":protected] => int(1)
  ["birthday":protected] => object(DateTime)#5 (3) {
    ["date"] => string(26) "1979-12-19 00:00:00.000000"
    ["timezone_type"] => int(3)
    ["timezone"] => string(13) "Europe/Berlin"
}

MySQL save datetime as unix timestamp and return all dates as timestamps if we get it as string , then we are convert in timestamp

Example: - date('m/d/Y H:i:s', 1541843467);

Related