EC-CUBE3のカテゴリ画像アップロードプラグイン の作成

## バージョン
3.0.16

## 動機
画像アップロードのプラグインについての記事がなかったのでまとめようと思いました。
職場の上司からはプラグイン作成は簡単だよと言われたんですが、かなり難しかった（フレームワークの概念を認識するまで）ので備忘録で。

## 参考
-  [EC-CUBE プラグインチュートリアル](https://doc.ec-cube.net/plugin_tutorial)

## 画像アップロードプラグイン作成
上記のプラグインチュートリアルでステップバイステップで、カテゴリーにコメントを追加できるプラグインが作成でき、画像アップロード機能はこれに追加。
まず上記チュートリアルを実施してください。

### チュートリアル後のファイル構成

[EC-CUBE プラグインチュートリアル](https://doc.ec-cube.net/plugin_tutorial)から引用

```
プラグインルートフォルダ  
├── CategoryContentEvent.php  
├── config.yml  
├── Entity  
│      └── CategoryContent.php  
├── event.yml  
├── PluginManager.php  
├── Repository  
│      └── CategoryContentRepository.php  
├── Resource  
│       └── doctrine  
│              ├── migration  
│              │       └── Version20160218160500.php  
│              └── Plugin.CategoryContent.Entity.CategoryContent.dcm.yml  
│  
└── ServiceProvider  
        └── CategoryContentServiceProvider.php
```

#### config.yml

```config.yml

name: カテゴリコンテンツプラグイン
code: CategoryContent
version: 1.0.0
orm.path:
    - /Resource/doctrine
event: CategoryContentEvent
service:
    - CategoryContentServiceProvider

```

#### event.yml

```event.yml
# カテゴリコントローラのフックポイト
admin.product.category.index.initialize:
    - [onAdminProductCategoryIndexInit, NORMAL]

# フォーム登録のために利用するフックポイント
admin.product.category.index.complete:
    - [onAdminProductCategoryIndexComplete, NORMAL]

# 商品一覧ページ表示のフックポイント
Product/list.twig:
    - [onRenderProductList, NORMAL]
```

#### PluginManager.php

```PluginManager.php

<?php

namespace Plugin\CategoryContent;

use Eccube\Plugin\AbstractPluginManager;

class PluginManager extends AbstractPluginManager
{
    // インストール時に、指定の処理を実行できます。(今回はなし)
    public function install($config, $app)
    {
    }

    // アンインストール時にマイグレーションの「down」メソッドを実行します
    public function uninstall($config, $app)
    {
        $this->migrationSchema($app, __DIR__.'/Resource/doctrine/migration', $config['code'], 0);
    }

    // プラグイン有効時に、マイグレーションの「up」メソッドを実行します
    public function enable($config, $app)
    {
        $this->migrationSchema($app, __DIR__.'/Resource/doctrine/migration', $config['code']);
    }

    // プラグイン無効時に、指定の処理 ( ファイルの削除など ) を実行できます。(今回はなし)
    public function disable($config, $app)
    {
    }

    // プラグインアップデート時に、指定の処理を実行できます。(今回はなし)
    public function update($config, $app)
    {
    }
}

```

#### CategoryContentEvent.php

```CategoryContentEvent.php

<?php

namespace Plugin\CategoryContent;

use Eccube\Application;
use Eccube\Common\Constant;
use Eccube\Entity\Category;
use Eccube\Event\EventArgs;
use Eccube\Event\TemplateEvent;
use Plugin\CategoryContent\Entity\CategoryContent;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class CategoryContentEvent
{
    /**
     * プラグインが追加するフォーム名
     */
    const CATEGORY_CONTENT_TEXTAREA_NAME = 'plg_category_content';

    /** @var \Eccube\Application $app */
    private $app;

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

    public function onAdminProductCategoryIndexInit(EventArgs $event)
    {
        /** @var Category $target_category */
        $TargetCategory = $event->getArgument('TargetCategory');
        $id = $TargetCategory->getId();

        $CategoryContent = null;

        // IDの有無で登録か編集かを判断
        if ($id) {
            // カテゴリ編集時は初期値を取得
            $CategoryContent = $this->app['category_content.repository.category_content']->find($id);
        }

        // カテゴリ新規登録またはコンテンツが未登録の場合
        if (is_null($CategoryContent)) {
            $CategoryContent = new CategoryContent();
        }

        // フォームの追加
        /** @var FormInterface $builder */
        // FormBuilderの取得
        $builder = $event->getArgument('builder');
        // 項目の追加
        $builder->add(
            self::CATEGORY_CONTENT_TEXTAREA_NAME,
            'textarea',
            array(
                'required' => false,
                'label' => false,
                'mapped' => false,
                'attr' => array(
                    'placeholder' => 'コンテンツを入力してください(HTMLタグ使用可)',
                ),
            )
        );

        // 初期値を設定
        $builder->get(self::CATEGORY_CONTENT_TEXTAREA_NAME)->setData($CategoryContent->getContent());

    }


    /**
     * 管理画面：カテゴリ登録画面で、登録処理を行う.
     *
     * @param EventArgs $event
     */
    public function onAdminProductCategoryIndexComplete(EventArgs $event)
    {
        /** @var Application $app */
        $app = $this->app;
        /** @var Category $target_category */
        $TargetCategory = $event->getArgument('TargetCategory');
        /** @var FormInterface $form */
        $form = $event->getArgument('form');

        // 現在のエンティティを取得
        $id = $TargetCategory->getId();
        // フォーム値のIDをもとに登録カテゴリ情報を取得
        $CategoryContent = $app['category_content.repository.category_content']->find($id);
        if (is_null($CategoryContent)) {
            $CategoryContent = new CategoryContent();
        }

        // エンティティを更新
        $CategoryContent
            ->setId($id)
            ->setContent($form[self::CATEGORY_CONTENT_TEXTAREA_NAME]->getData());

        // DB更新
        $app['orm.em']->persist($CategoryContent);
        $app['orm.em']->flush($CategoryContent);
    }

    /**
     * 商品一覧画面にカテゴリコンテンツを表示する.
     *
     * @param TemplateEvent $event
     */
    public function onRenderProductList(TemplateEvent $event)
    {
        $parameters = $event->getParameters();

        // カテゴリIDがない場合、レンダリングしない
        if (is_null($parameters['Category'])) {
            return;
        }

        // 登録がない、もしくは空で登録されている場合、レンダリングをしない
        $Category = $parameters['Category'];
        $CategoryContent = $this->app['category_content.repository.category_content']
            ->find($Category->getId());
        if (is_null($CategoryContent) || $CategoryContent->getContent() == '') {
            return;
        }

        // twigコードにカテゴリコンテンツを挿入
        $snipet = '<div class="row">{{ CategoryContent.content | raw }}</div>';
        $search = '<div id="result_info_box"';
        $replace = $snipet.$search;
        $source = str_replace($search, $replace, $event->getSource());
        $event->setSource($source);

        // twigパラメータにカテゴリコンテンツを追加
        $parameters['CategoryContent'] = $CategoryContent;
        $event->setParameters($parameters);
    }
}

```

#### Entity/CategoryContent.php

```Entity/CategoryContent.php

<?php

namespace Plugin\CategoryContent\Entity;

class CategoryContent extends \Eccube\Entity\AbstractEntity
{
    private $id;

    private $content;

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

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

        return $this;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }
}

```

#### ServiceProvider/CategoryContentServiceProvider.php

```ServiceProvider/CategoryContentServiceProvider.php

<?php

namespace Plugin\CategoryContent\ServiceProvider;

use Eccube\Application;
use Silex\Application as BaseApplication;
use Silex\ServiceProviderInterface;

class CategoryContentServiceProvider implements ServiceProviderInterface
{
    public function register(BaseApplication $app)
    {
        // Repository
        $app['category_content.repository.category_content'] = $app->share(function () use ($app) {
            return $app['orm.em']->getRepository('Plugin\CategoryContent\Entity\CategoryContent');
        });

    }

    public function boot(BaseApplication $app)
    {
    }
}

```

#### Repository/CategoryContentRepository.php

```

<?php

namespace Plugin\CategoryContent\Repository;

use Doctrine\ORM\EntityRepository;

class CategoryContentRepository extends EntityRepository
{
}

```

#### Resource/doctrine/migration/Version20160218160500.php

```Resource/doctrine/migration/Version20160218160500.php

<?php

namespace DoctrineMigrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
use Eccube\Common\Constant;

class Version20160218160500 extends AbstractMigration
{
    // 対象のエンティティを指定
    protected $entities = array(
        'Plugin\CategoryContent\Entity\CategoryContent',
    );

    public function up(Schema $schema)
    {
        // テーブルの生成
        $app = \Eccube\Application::getInstance();
        $meta = $this->getMetadata($app['orm.em']);
        $tool = new SchemaTool($app['orm.em']);
        $tool->createSchema($meta);
    }

    public function down(Schema $schema)
    {
        $app = \Eccube\Application::getInstance();
        $meta = $this->getMetadata($app['orm.em']);

        $tool = new SchemaTool($app['orm.em']);
        $schemaFromMetadata = $tool->getSchemaFromMetadata($meta);

        // テーブル削除
        foreach ($schemaFromMetadata->getTables() as $table) {
            if ($schema->hasTable($table->getName())) {
                $schema->dropTable($table->getName());
            }
        }

        // シーケンス削除
        foreach ($schemaFromMetadata->getSequences() as $sequence) {
            if ($schema->hasSequence($sequence->getName())) {
                $schema->dropSequence($sequence->getName());
            }
        }
    }

    protected function getMetadata(EntityManager $em)
    {
        $meta = array();
        foreach ($this->entities as $entity) {
            $meta[] = $em->getMetadataFactory()->getMetadataFor($entity);
        }

        return $meta;
    }
}

```
  
#### Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.yml

```Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.yml

Plugin\CategoryContent\Entity\CategoryContent:
    type: entity
    table: plg_category_content
    repositoryClass: Plugin\CategoryContent\Repository\CategoryContentRepository
    id:
        id:
            type: integer
            nullable: false
            options:
                unsigned: false
            id: true
            column: category_id
            generator:
                strategy: NONE
    fields:
        content:
            type: text
            nullable: true
    lifecycleCallbacks: {  }

```

#### チュートリアル後の画面
![スクリーンショット 2018-08-18 15.50.53.png](https://qiita-image-store.s3.amazonaws.com/0/59304/9d90dae6-d552-e91f-286e-e654844a1a0b.png)

### 画像アップロード機能の追加
- user_data内に保存
    - html/user_data/category_image/{id}/カテゴリー画像

```
html/user_data/
└── category_image
    ├── 1
    │   └── category01.png
    ├── 2
    │   └── category01.png
    ├── 3
    │   └── category01.png
    └── 4
        └── category01.png
```

- 後々管理画面の、コンテンツ管理＞ファイル管理からも編集可能

#### 画像ファイルのパスを保存するカラムを追加する

- チュートリアルで作ったplg_category_contentテーブルにimageカラムを追加
    - Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.ymlを編集
    - CategoryContent.phpを編集
    - CategoryContentプラグインを一度アンインストールし、再度インストールして有効化する

##### Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.ymlを編集

```Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.yml

Plugin\CategoryContent\Entity\CategoryContent:
    type: entity
    table: plg_category_content
    repositoryClass: Plugin\CategoryContent\Repository\CategoryContentRepository
    id:
        id:
            type: integer
            nullable: false
            options:
                unsigned: false
            id: true
            column: category_id
            generator:
                strategy: NONE
    fields:
        content:
            type: text
            nullable: true
    #下記に追加
        image:
            type: text
            nullable: true
    lifecycleCallbacks: {  }

```

##### CategoryContent.phpを編集

```CategoryContent.php

<?php

namespace Plugin\CategoryContent\Entity;

class CategoryContent extends \Eccube\Entity\AbstractEntity
{
    private $id;

    private $content;

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

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

        return $this;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }

    #下記に追加
    private $image;

    public function getImage()
    {
        return $this->image;
    }

    public function setImage($image)
    {
        $this->image = $image;

        return $this;
    }
}

```

##### CategoryContentプラグインを一度アンインストールし、再度インストールして有効化する

```
$ php app/console plugin:develop uninstall --code CategoryContent
success
$ php app/console plugin:develop install --code CategoryContent
success
$ php app/console plugin:develop enable --code CategoryContent
success
```

<img width="317" alt="スクリーンショット 2018-08-18 16.29.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/59304/ac0fe859-24fd-6243-2ed2-bed6b3a78535.png">

##### フォームを追加

- CategoryContentEvent.php
    - 関数 onAdminProductCategoryIndexInit を編集

```CategoryContentEvent.php

    public function onAdminProductCategoryIndexInit(EventArgs $event)
    {
        /** @var Category $target_category */
        $TargetCategory = $event->getArgument('TargetCategory');
        $id = $TargetCategory->getId();

        $CategoryContent = null;

        // IDの有無で登録か編集かを判断
        if ($id) {
            // カテゴリ編集時は初期値を取得
            $CategoryContent = $this->app['category_content.repository.category_content']->find($id);
        }

        // カテゴリ新規登録またはコンテンツが未登録の場合
        if (is_null($CategoryContent)) {
            $CategoryContent = new CategoryContent();
        }

        // フォームの追加
        /** @var FormInterface $builder */
        // FormBuilderの取得
        $builder
            ->add(
            self::CATEGORY_CONTENT_TEXTAREA_NAME,
            'textarea',
            array(
                'required' => false,
                'label' => false,
                'mapped' => false,
                'attr' => array(
                    'placeholder' => 'コンテンツを入力してください(HTMLタグ使用可)',
                ),
            ))
            ->add(
                'plg_category_image',
                'file',
                array(
                    'label' => 'カテゴリー画像',
                    'required' => false,
                    'mapped' => false,
            ))
        ;

        // 初期値を設定
        $builder->get(self::CATEGORY_CONTENT_TEXTAREA_NAME)->setData($CategoryContent->getContent());

    }

```

![スクリーンショット 2018-08-19 0.43.46.png](https://qiita-image-store.s3.amazonaws.com/0/59304/d1ced305-3f98-57e4-2e7c-777ff57d6065.png)

上記から・・・・・

![スクリーンショット 2018-08-19 0.44.02.png](https://qiita-image-store.s3.amazonaws.com/0/59304/d0983088-c8a1-ec6b-41a5-40997ef8b539.png)

カテゴリー画像のファイル選択が追加された。

##### データベースに登録できるようにする

- CategoryContentEvent.php
    - 関数 onAdminProductCategoryIndexComplete を編集

```CategoryContentEvent.php

    public function onAdminProductCategoryIndexComplete(EventArgs $event)
    {
        /** @var Application $app */
        $app = $this->app;
        /** @var Category $target_category */
        $TargetCategory = $event->getArgument('TargetCategory');
        /** @var FormInterface $form */
        $form = $event->getArgument('form');

        // 現在のエンティティを取得
        $id = $TargetCategory->getId();
        // フォーム値のIDをもとに登録カテゴリ情報を取得
        $CategoryContent = $app['category_content.repository.category_content']->find($id);
        if (is_null($CategoryContent)) {
            $CategoryContent = new CategoryContent();
        }

        // エンティティを更新
        if ($form['plg_category_image']->getData() === null) {
            $CategoryContent
                ->setId($id)
                ->setContent($form[self::CATEGORY_CONTENT_TEXTAREA_NAME]->getData());
        } else {
            $CategoryContent
                ->setId($id)
                ->setContent($form[self::CATEGORY_CONTENT_TEXTAREA_NAME]->getData())
                ->setImage("/category_image/" . $form->getData()->getId() . '/' . $form['plg_category_image']->getData()->getClientOriginalName());
        }

        // DB更新
        $app['orm.em']->persist($CategoryContent);
        $app['orm.em']->flush($CategoryContent);
    }

```

これで、DBへ保存されるようになった。
DBのimageカラムには"/category_image/{id}/{画像名}"で保存される。

##### 画像ファイルをuser_data内に保存できるようにする。

- user_data直下にcategory_imageフォルダを追加しておく

![スクリーンショット 2018-08-19 1.10.35.png](https://qiita-image-store.s3.amazonaws.com/0/59304/5e9ce93a-1b14-299d-15b8-c0350f628ceb.png)


```/src/Eccube/Controller/Admin/Product/CategoryController.php

106行目辺りに追加 if ($status) 内

// CategoryContentX pluginにて画像アップロードの為の設定
$file = $form['plg_category_image']->getData();
$upload_path = __DIR__.'/../../../../../html/user_data/category_image/' . $event->getArgument('TargetCategory')->getId();
if ($file) {
    if (!file_exists($upload_path)) {
        mkdir($upload_path, 0755);
    }
    $file->move($upload_path , $file->getClientOriginalName());
}
// CategoryContentX pluginにて画像アップロードの為の設定


```

これで、user_data内にファイル保存されるようになった。
user_data/category_image/{id}/{画像名}"で保存される。

![スクリーンショット 2018-08-19 1.13.54.png](https://qiita-image-store.s3.amazonaws.com/0/59304/90dc5af2-0bfd-aa84-7408-9f3903245f5b.png)

##### 編集画面で画像ファイルを表示させる

- イベント追加

```event.yml

Admin/Product/category.twig:
    - [onadmin_product_category_twig, NORMAL]

```

- CategoryContentEvent.php
    - 関数 onadmin_product_category_twig を追加

```CategoryContentEvent.php

    public function onadmin_product_category_twig(TemplateEvent $event)
    {
        $parameters = $event->getParameters();
        $a = explode("/", $_SERVER["REQUEST_URI"]);
        if (end($a) === "edit") {
            //カテゴリIDがない場合、レンダリングしない
            if (is_null($parameters['TargetCategory'])) {
                return;
            }
            //登録がない、もしくは空で登録されている場合、レンダリングをしない
            $Category = $parameters['TargetCategory'];
            $CategoryContent = $this->app['category_content.repository.category_content']->find($Category->getId());
            if (is_null($CategoryContent) || $CategoryContent->getImage() == '') {
                return;
            }
            // twigコードにカテゴリコンテンツを挿入
            $snipet = '<div class="col-md-12 form-group"><p>現在適用されている画像</p><div><img src="{{app.config.user_data_urlpath ~ CategoryContent.image}}" width="300px"></div></div>';
            $search = '</form>';
            $replace = $snipet . $search;
            $source = str_replace($search, $replace, $event->getSource());
            $event->setSource($source);
            // twigパラメータにカテゴリコンテンツを追加
            $parameters['CategoryContent'] = $CategoryContent;
            $event->setParameters($parameters);
        }
        $event->setParameters($parameters);

    }


```

![スクリーンショット 2018-08-19 1.21.33.png](https://qiita-image-store.s3.amazonaws.com/0/59304/81a0204f-5c8e-5a96-d5ed-b17986cbddb0.png)

編集画面にて、登録されていれば画像が表示されるようになった。

## 商品一覧での画像の取得

```event.yml
Product/list.twig:
    - [onRenderProductList, NORMAL]
```

```CategoryContentEvent.php

class CategoryContentEvent
{
.
.
.
.
.
.
    /**
     * 商品一覧画面にカテゴリコンテンツを表示する.
     */
    public function onRenderProductList(TemplateEvent $event)
    {
        $parameters = $event->getParameters();

        // カテゴリIDがない場合、レンダリングしない
        if (is_null($parameters['Category'])) {
            return;
        }

        $Category = $parameters['Category'];
        $CategoryContent = $this->app['category_content.repository.category_content']->find($Category->getId());

        // twigパラメータにカテゴリコンテンツを追加
        $parameters['CategoryContent'] = $CategoryContent;        
        $event->setParameters($parameters);
    }

}

```

```app/template/default/Product/list.twig
{# カテゴリー画像を表示 #}
<img src="{{app.config.user_data_urlpath ~ CategoryContent.image}}">
```

## 今後の課題
- 一部src配下のコアコードを触っているので、これもプラグインとして完結させたい
- 画像の削除機能が無いため残ったままになる。

## 所感
Symfony2を使ったことがなかったので、世界観を掴むまでに時間がかかった。
これからEC-CUBE3の開発をされる方は、まずSymfony2を理解してからの状態で臨むのがおすすめです。