EC-CUBE3の管理機能を増やすときいじるところ

EC-CUBE（というかSymfony2？）の管理画面に機能を追加するときに発生する作業。
意外にトータルで紹介しているのが少なかった自分のメモ代わりに共有しておきます。
（EC-CUBE3のオフィシャルのドキュメントはわかりにくかったのであんま見てませんm(_ _)m）
一応DoctrineとかORMとか永続化とか難しいことは考えなくてもできると思います。
**※当然理解したほうがいいよ！！**

該当バージョンは3.0.16です。
カテゴリ管理を基に「ブランド管理」機能を追加する想定で記載します。
また、以下手順は順不同です。

#DBにテーブル追加
「dtb_brand」として追加します。カテゴリを基に以下のようにします。

```
CREATE TABLE `dtb_brand` (
  `id` int(11) NOT NULL,
  `creator_id` int(11) DEFAULT NULL,
  `name` text NOT NULL,
  `rank` int(11) NOT NULL,
  `create_date` datetime NOT NULL,
  `update_date` datetime NOT NULL,
  `del_flg` smallint(6) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
階層のないカテゴリみたいなものです。
マイグレーションで追加してもいいですが、~~カラムの情報をチマチマ書くのがメンドかったので~~今回は普通に追加しました。

#Entityファイル追加
テーブルデータをオブジェクトとして扱えるようにするためにマッピングさせるクラスファイルです。
/src/Eccube/Entity/Brand.phpを以下のように追加します。

```
namespace Eccube\Entity;

use Eccube\Entity\AbstractEntity;

class Brand extends AbstractEntity
{
    private $id;
    private $name;

    /**
     * @var \DateTime
     */
    private $create_date;

    /**
     * @var \DateTime
     */
    private $update_date;

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->getName();
    }

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

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

    public function setName ($name)
    {
        $this->name = $name;
        return $this;
    }

    public function getName ()
    {
        return $this->name;
    }
    //〜〜後略〜〜
}
```
ま、項目を減らしただけですね。。。

#Repositoryファイル追加
未だにわかりやすい言い方がわからないファイル。。。
/src/Eccube/Repository/BrandRepository.phpを以下のように追加します。

```
namespace Eccube\Repository;

use Doctrine\ORM\EntityRepository;
use Eccube\Application;
use Eccube\Entity\Brand;

/**
 * BrandRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class BrandRepository extends EntityRepository
{
    /**
     * @var \Eccube\Application
     */
    protected $app;

    public function setApplication(Application $app)
    {
        $this->app = $app;
    }

    /**
     * ブランド一覧を取得する.*
     **
     * @return \Eccube\Entity\Brand[] ブランドの配列
     */
    public function getList()
    {
        $options = $this->app['config']['doctrine_cache'];
        $lifetime = $options['result_cache']['lifetime'];

        $qb = $this->createQueryBuilder('b1')
            ->select('b1')
            ->orderBy('b1.rank', 'DESC');

        $Brands = $qb->getQuery()
            ->useResultCache(true, $lifetime)
            ->getResult();

        return $Brands;
    }
    //〜〜後略〜〜
}

```
後略の部分はCategoryRepository.phpのcategoryとかCategoryの記述をbrand、Brandに変える感じです。
恐らく不要な処理もありますが一旦そのまま残しておきます。

#doctrine/〜〜〜.ymlファイル追加
（このファイルなんていうんかな・・・）
テーブルの制限などを記載するファイルです。（←よくわかってない）
/src/Eccube/Resource/doctrine/Eccube.Entity.Brand.dcm.ymlを以下のように追加します。

```
Eccube\Entity\Brand:
    type: entity
    table: dtb_brand
    repositoryClass: Eccube\Repository\BrandRepository
        〜〜後略〜〜
```
上記が変更すべき箇所です。
後略の部分は〜〜〜以下同文。

#Controllerファイル追加
/src/Eccube/Controller/Admin/Product/BrandController.phpを以下のように追加します。

```

namespace Eccube\Controller\Admin\Product;

use Eccube\Application;
use Eccube\Controller\AbstractController;
use Eccube\Entity\Brand;
use Eccube\Entity\Master\CsvType;
use Eccube\Util\CommonUtil;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class BrandController extends AbstractController
{
    public function index(Application $app, Request $request, $id = null)
    {
        if ($id) {
            $TargetBrand = $app['eccube.repository.brand']->find($id);
            if (!$TargetBrand) {
                throw new NotFoundHttpException('ブランドが存在しません');
            }
        } else {
            $TargetBrand = new \Eccube\Entity\Brand();
        }

        //
        $builder = $app['form.factory']
            ->createBuilder('admin_brand', $TargetBrand);

        $form = $builder->getForm();

        //
        if ($request->getMethod() === 'POST') {
            $form->handleRequest($request);
            if ($form->isValid()) {

                log_info('ブランド登録開始', array($id));
                $status = $app['eccube.repository.brand']->save($TargetBrand);

                if ($status) {

                    log_info('ブランド登録完了', array($id));

                    $app->addSuccess('admin.brand.save.complete', 'admin');

                    return $app->redirect($app->url('admin_product_brand'));
                } else {
                    log_info('ブランド登録エラー', array($id));
                    $app->addError('admin.brand.save.error', 'admin');
                }
            }
        }

        $Brands = $app['eccube.repository.brand']->getList();

        return $app->render('Product/brand.twig', array(
            'form' => $form->createView(),
            'Brands' => $Brands,
            'TargetBrand' => $TargetBrand,
        ));
    }

    public function delete(Application $app, Request $request, $id)
    {
        $this->isTokenValid($app);

        $TargetBrand = $app['eccube.repository.brand']->find($id);
        if (!$TargetBrand) {
            $app->deleteMessage();
            return $app->redirect($app->url('admin_product_brand'));
        }
        $Parent = $TargetBrand->getParent();

        log_info('ブランド削除開始', array($id));

        $status = $app['eccube.repository.brand']->delete($TargetBrand);

        if ($status === true) {

            log_info('ブランド削除完了', array($id));

            $app->addSuccess('admin.brand.delete.complete', 'admin');
        } else {
            log_info('ブランド削除エラー', array($id));
            $app->addError('admin.brand.delete.error', 'admin');
        }
    }

    public function moveRank(Application $app, Request $request)
    {
        if ($request->isXmlHttpRequest()) {
            $ranks = $request->request->all();
            foreach ($ranks as $categoryId => $rank) {
                /* @var $Brand \Eccube\Entity\Brand */
                $Brand = $app['eccube.repository.brand']
                    ->find($categoryId);
                $Brand->setRank($rank);
                $app['orm.em']->persist($Brand);
            }
            $app['orm.em']->flush();
        }
        return true;
    }
}

```

ほぼCategoryControllersの内容をブランド用に書き換えるだけでいけますが、
今回は説明のためCSV出力に関する記述と、コピペしただけだと残る以下のようなイベントに関する記述は今回は省略しています。一応なくても動きます。

```
$event = new EventArgs(
    array(
        'builder' => $builder,
        'TargetBrand' => $TargetBrand,
    ),
    $request
);
$app['eccube.event.dispatcher']->dispatch(EccubeEvents::ADMIN_PRODUCT_CATEGORY_INDEX_INITIALIZE, $event);
```

#FormTypeファイル追加
入力画面の仕様を決めるファイルです。
/src/Eccube/Form/type/Admin/BrandType.phpを以下のように追加します。

```
namespace Eccube\Form\Type\Admin;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints as Assert;

class BrandType extends AbstractType
{
    private $config;

    public function __construct($config)
    {
        $this->config = $config;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text', array(
                'label' => 'ブランド名',
                'constraints' => array(
                    new Assert\NotBlank(),
                    new Assert\Length(array(
                        'max' => $this->config['stext_len'],
                    )),
                ),
            ));
    }

    /**
     * {@inheritdoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Eccube\Entity\Brand',
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'admin_brand';
    }
}

```
上記サンプルにはないですが、mappedオプションをfalseにするとテーブルのカラムと連動しなくなります。
DBに格納する前に値を編集する必要がある場合などに使用します。
最後のgetNameメソッドに記載した「admin_brand」はBrandController.phpの

```
$builder = $app['form.factory']->createBuilder('admin_brand', $TargetBrand);
```

の引数の内容と合わせておきます。

#twigファイル追加
テンプレートファイル、
/src/Eccube/Resource/template/admin/Product/brand.twigをcategory.twigからコピペして追加します。
ほぼほぼ変わらないのでソースは省略します。

#ServiceProvider追加登録
先ほど追加したEccube\Entity\BrandとEccube\Form\Type\Admin\BrandTypeを
/src/Eccube/ServiceProvider/EccubeServiceProvider.phpに登録します。

```
〜〜前略〜〜
$app['eccube.repository.brand'] = $app->share(function () use ($app) {
    $BrandRepository = $app['orm.em']->getRepository('Eccube\Entity\Brand');
    $BrandRepository->setApplication($app);

    return $BrandRepository;
});
〜〜中略〜〜
$types[] = new \Eccube\Form\Type\Admin\BrandType($app['config']);
〜〜後略〜〜
```

ここに追記すると、EC-CUBE内で使用しますよー、という宣言的なことになります。

#nav.yml.distにメニュー追加
ナビゲーションエリアにブランド管理のメニューを追加します。
/src/Eccube/Resource/config/nav.yml.distを編集します。
以下をカテゴリ登録の次くらいに追記します。

```
- id: brand
  name: ブランド登録
  url: admin_product_brand
```

#ControlProviderにルーティング追加登録
ナビゲーションにメニューを追加しただけでは画面が遷移しないので、
/src/Eccube/ControllerProvider/AdminControllerProvider.phpの
これまたカテゴリ登録の次くらいに以下を追記します。

```
$c->match('/product/brand', '\Eccube\Controller\Admin\Product\BrandController::index')->bind('admin_product_brand');
$c->match('/product/brand/{id}/edit', '\Eccube\Controller\Admin\Product\BrandController::index')->assert('id', '\d+')->bind('admin_product_brand_edit');
$c->delete('/product/brand/{id}/delete', '\Eccube\Controller\Admin\Product\BrandController::delete')->assert('id', '\d+')->bind('admin_product_brand_delete');
$c->post('/product/brand/rank/move', '\Eccube\Controller\Admin\Product\BrandController::moveRank')->bind('admin_product_brand_rank_move');
$c->match('/product/brand/export', '\Eccube\Controller\Admin\Product\BrandController::export')->bind('admin_product_brand_export');
```

このURL叩いたら、コントローラーのこのメソッド見に行ってねー、的な設定です。

#メッセージファイル編集
主にBrandController.php登録時やエラー時に表示するメッセージの編集を行います。
/src/Eccube/Resource/locale/message.ymlを編集します。

```
admin.brand.up.complete: ブランドを上へ移動しました。
admin.brand.up.error: ブランドを上へ移動できませんでした。
admin.brand.down.complete: ブランドを下へ移動しました。
admin.brand.down.error: ブランドを下へ移動できませんでした。
admin.brand.save.complete: ブランドを保存しました。
admin.brand.save.error: ブランドを保存できませんでした。
admin.brand.delete.complete: ブランドを削除しました。
admin.brand.delete.error: ブランドを削除できませんでした。
```
これくらいあればいいかな、と。
追記する場所はどこでもいいと思います。
わかりやすく、カテゴリの次ぐらいがいいんじゃないでしょうか。

#完成
完成すると下図のような画面になると思います。
<img width="1497" alt="Screenshot at Aug 06 22-12-57.png" src="https://qiita-image-store.s3.amazonaws.com/0/121373/8b61f839-d4df-48e4-be62-bc72b958543f.png">

#まとめ
画面を作るだけなら紹介した手順で既存のファイルや記述をコピペするだけで大体できるかな、と。
編集するファイルはBrandController.phpで省略したイベント関係も含めると11個以上になり少々手間はかかります。
ただ、ややこしい入力項目などがなければこの手順でほぼ出来上がってしまうのでEC-CUBE2よりかは開発は簡単な印象。（慣れれば小一時間くらいでできるかも？です。知らんけど←）

新しい機能はプラグインとして開発したほうが早い場面もあると思いますが、
僕はこのコアコードを追加編集する方法の方が早かったのでどなたかの参考になれば。

あ、あと、冒頭にも書きましたが、EC-CUBE3はSymfony2ベースでできてますが、
そんなこと知らない、わからない人もいると思いますので、敢えて「EC-CUBE3の」とタイトルとタグにつけました。

EC-CUBE3の情報って意外に少ないよね・・・。