Symfony3で論理削除の導入

## 概要
仕事で論理削除を実装する必要が出てきたので調べたついでに残しておく

## 環境
- PHP → 7.1.23
- symfony/symfony → v3.4.18
- doctrine/orm → v2.6.2
- gedmo/doctrine-extensions → v2.4.36
- stof/doctrine-extensions-bundle → v1.3.0

## 設定
### Doctrine拡張のインストール
```
$ composer require stof/doctrine-extensions-bundle
```

### バンドルの有効化
```app/AppKernel.php
<?php 
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // ...
            
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
            
            // ...
        ];
    {
}
```

### Configに設定
```app/config/config.yml
doctrine:
    orm:
        # フィルターを利用したい場合はこれも書く
        filters:
            softdeleteable:
                class: 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'
                enabled: true

# 論理削除設定
stof_doctrine_extensions:
    default_locale: '%locale%'
    orm:
        default:
            softdeleteable: true
```

### Entityの準備
```src/AppBundle/Entity/Product.php
<?php
namespace AppBundle\Entity;

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

/**
 * Class Product
 * @package AppBundle\Entity
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 * @ORM\Table(name="product")
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=true)
 */
class Product
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;
    
    /**
     * @var \DateTime
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $deletedAt;
    
    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
    
    /**
     * @return \DateTime
     */
    public function getDeletedAt()
    {
        return $this->deletedAt;
    }

    /**
     * @param \DateTime $deletedAt
     */
    public function setDeletedAt($deletedAt)
    {
        $this->deletedAt = $deletedAt;
    }
}
```

### DBに反映
```
$ bin/console doctrine:schema:update --force
```

## 使用例(フィルターと論理削除)
```src/AppBundle/Controller/Admin/ProductController.php
<?php 

/**
 * Class ProductController
 * @package AppBundle\Controller\Admin
 * @Route("/admin/product")
 */
class ProductController extends Controller
{
    /**
     * 製品一覧(フィルター適用例)
     * @Route("/", name="admin_product_list")
     * @Method("GET")
     *
     * @param Request $request
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function listAction(Request $request)
    {
        // 製品一覧取得
        /** @var EntityManager $em */
        $em = $this->getDoctrine()->getManager();
        // 論理削除したデータも含めて取得したい場合はフィルターを適用する(soft-deleteableは古い記述の仕方っぽい)
        $em->getFilters()->disable('softdeleteable');
        $products = $em->getRepository(Product::class)->findAll();

        dump($products)
        
        // ...
    }
    // ...

    /**
     * 製品削除(論理削除例)
     *
     * @Route("/{id}/delete", name="admin_product_delete")
     * @Method("DELETE")
     * @param Request $request
     * @param Product $product
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function deleteAction(Request $request, Product $product)
    {
        // ...

        // Entityで指定さえしておけば特別な記述なく論理削除できる
        $em = $this->getDoctrine()->getManager();
        $em->remove($product);
        $em->flush();

        // 現バージョンは2回削除を実行する事で物理削除となる(バグ？)
        // $em->remove($product);
        // $em->flush();

        // 正常動作はこちらのはず
        // $em->getFilters()->disable('softdeleteable');
        // $em->remove($product);
        // $em->flush();

        // ...
    }
}
```

## 注意点
- 論理削除したデータを取得しないのはORMを利用する場合だけなのでDQLとクエリビルダの時はwhereで指定する

## 参考文献
- [StofDoctrineExtensionsBundle](https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html)
- [SoftDeleteable behavior extension for Doctrine 2](https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/softdeleteable.md)
- [Symfony2 SoftDeleteableを一時的に無効化して、物理削除したい](https://teratail.com/questions/30168)
- [Symfony3でSoftDeleteableを一時的に無効にする](https://qiita.com/setouchi/items/477a28143c1bcafd2d9e)