DoctrineとEloquent比較大全21: レコードの更新日時・作成日時を自動で保存する

day21（から4日遅れ）の今日は、DoctrineやEloquentでレコードを保存するときに、作成日時・更新日時を自動保存する方法を見ていきます。

## Doctrine

```php:Entity/Book.php
<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'books')]
#[ORM\HasLifecycleCallbacks]
class Book
{
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    private int $id;

    #[ORM\Column(type: 'string', length: 255, nullable: false)]
    private string $title;

    #[ORM\Column(type: 'integer')]
    private int $price;

    #[ORM\ManyToOne(targetEntity: Author::class)]
    #[ORM\JoinColumn(onDelete: 'CASCADE')]
    private Author $author;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $description;

    #[ORM\OneToMany(mappedBy: 'book', targetEntity: Reaction::class, cascade: ['persist'], orphanRemoval: true)]
    private Collection $reactions;

    #[ORM\Column(name: 'created_at', type: 'datetime_immutable')]
    private \DateTimeImmutable $createdAt;

    #[ORM\Column(name: 'updated_at', type: 'datetime_immutable')]
    private \DateTimeImmutable $updatedAt;

    public function __construct()
    {
        $this->reactions = new ArrayCollection();
    }

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

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

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

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

    public function getPrice(): int
    {
        return $this->price;
    }

    public function setPrice(int $price): void
    {
        $this->price = $price;
    }

    public function getAuthor(): Author
    {
        return $this->author;
    }

    public function setAuthor(Author $author): void
    {
        $this->author = $author;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): void
    {
        $this->description = $description;
    }

    /**
     * @return Collection<Reaction>
     */
    public function getReactions(): Collection
    {
        return $this->reactions;
    }

    public function setReactions(Collection $reactions): void
    {
        $this->reactions = $reactions;
    }

    public function addReaction(Reaction $reaction): void
    {
        if (!$this->reactions->contains($reaction)) {
            $reaction->setBook($this);
            $this->reactions->add($reaction);
        }
    }

    public function removeReaction(Reaction $reaction): void
    {
        $reaction->setBook(null);
        $this->reactions->removeElement($reaction);
    }

    #[ORM\PrePersist]
    public function prePersist(): void
    {
        $this->createdAt = new \DateTimeImmutable();
        $this->updatedAt = new \DateTimeImmutable();
    }

    #[ORM\PreUpdate]
    public function preUpdate(): void
    {
        $this->updatedAt = new \DateTimeImmutable();
    }

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeImmutable $createdAt): void
    {
        $this->createdAt = $createdAt;
    }

    public function getUpdatedAt(): \DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(\DateTimeImmutable $updatedAt): void
    {
        $this->updatedAt = $updatedAt;
    }
}
```

```php
<?php

declare(strict_types=1);

use App\Entity\Author;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;

require __DIR__.'/../vendor/autoload.php';

/** @var EntityManagerInterface $entityManager */
$entityManager = require __DIR__.'/bootstrap.php';

$author = new Author();
$author->setName('tanakahisateru');
$newBook = new Book();
$newBook->setTitle('ちょうぜつソフトウェア設計入門');
$newBook->setPrice(3080);
$newBook->setDescription('かわいい絵柄で釣ってオブジェクト指向をガッツリ解説する本');
$newBook->setAuthor($author);

/** @var EntityManagerInterface $entityManager */
$entityManager = require __DIR__.'/bootstrap.php';
$entityManager->persist($author);
$entityManager->persist($newBook);
$entityManager->flush();

$newBook->getCreatedAt(); // 登録日時
$newBook->getUpdatedAt(); // 更新日時
```

- DoctrineのLifecycleCallbackを設定することで実現できます。カラムや保存処理は自分で書く必要があります。
- 全部のエンティティにupdatedAt, createdAtを設定したい場合など、全エンティティに設定するのが面倒な場合はDoctrineExtensions <https://github.com/doctrine-extensions/DoctrineExtensions> の `Timestampable` を使うと楽に設定できます。

## Eloquent

```php:Models/Book.php
<?php

declare(strict_types=1);

namespace App\Models;

use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model;

/**
 * App\Models\Book
 *
 * @property int $id
 * @property string $title
 * @property int $price
 * @property Author $author
 * @property string|null $description
 * @property CarbonInterface $created_at
 * @property CarbonInterface $updated_at
 */
class Book extends Model
{
    public $fillable = [
        'title',
        'price',
        'author_id',
        'description',
    ];

    public $timestamps = true;

    public function author()
    {
        return $this->belongsTo(Author::class);
    }

    public function likes()
    {
        return $this->morphMany(Like::class, 'likable');
    }
}
```

```php
<?php

declare(strict_types=1);

use App\Models\Author;
use App\Models\Book;

require __DIR__.'/../vendor/autoload.php';
require __DIR__.'/bootstrap.php';

$author = new Author();
$author->name = 'tanakahisateru';
$author->save();
$newBook = new Book();
$newBook->title = 'ちょうぜつソフトウェア設計入門';
$newBook->price = 3080;
$newBook->description = 'かわいい絵柄で釣ってオブジェクト指向をガッツリ解説する本';
$newBook->author_id = $author->id;
$newBook->save();

$newBook->created_at; // 登録日時がセットされている
$newBook->updated_at; // 更新日時がセットされている
```

- Modelの `$timestamps` プロパティをtrueにセットするだけです。細かいオプション（カラム名を標準から変える等）はすべてModelの所定のプロパティを使って設定します。