DoctrineとEloquent比較大全22: 論理削除する

day22（から3日遅れ）の今日は論理削除のやり方について見ていきます。
論理削除とは、レコードを削除する操作を行ったとき、実際にはDBMS上からDELETEせずに削除フラグや削除日時をUPDATEで書き込むことで、「ゴミ箱に入れる」のように復活可能な削除を行うことです。

## Doctrine

```php:Enity/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;
use Gedmo\Mapping\Annotation\SoftDeleteable;

#[ORM\Entity]
#[ORM\Table(name: 'books')]
#[ORM\HasLifecycleCallbacks]
#[SoftDeleteable(fieldName: 'deletedAt')]
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;

    #[ORM\Column(name: 'deleted_at', type: 'datetime_immutable')]
    private ?\DateTimeImmutable $deletedAt = null;

    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;
    }

    public function getDeletedAt(): ?\DateTimeImmutable
    {
        return $this->deletedAt;
    }

    public function setDeletedAt(?\DateTimeImmutable $deletedAt): void
    {
        $this->deletedAt = $deletedAt;
    }

    public function isDeleted(): bool
    {
        return $this->deletedAt !== null;
    }
}

```

```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';

$book = $entityManager->find(Book::class, 11);
$entityManager->remove($book);
$entityManager->flush(); // DELETE FROM booksでなくUPDATE books set deleted_at = now() が発行される
```

- DoctrineExtensionsの `SoftDeleteable` を使っています。（ https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/softdeleteable.md ）
- SoftDeleteableのマッピングを付与し、`Book::$deletedAt` プロパティを追加しています。
- 通常の削除操作（ `$entityManager->remove($book); $entityManager->flush();` ）を行ったときに、DELETEでなくUPDATEでdeleted_atカラムに削除日時を書き込みます。SELECTのクエリには自動で `deleted_at IS NULL` の条件が追加され、削除されたレコードが除外された結果のみが出てきます。

## Eloquent

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

declare(strict_types=1);

namespace App\Models;

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

/**
 * 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
{
    use SoftDeletes;

    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\Book;

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

$book = Book::find(11);
$book->delete(); // DELETE FROM booksでなくUPDATE books set deleted_at = now() が発行される

```

- Modelに `SoftDeletes` トレイトをuseすることで論理削除が使えるようになります。
- 通常の削除操作（ `$book->delete();` ）を行ったときに、DELETEでなくUPDATEでdeleted_atカラムに削除日時を書き込みます。SELECTのクエリには自動で `deleted_at IS NULL` の条件が追加され、削除されたレコードが除外された結果のみが出てきます。