[Done]EC-CUBE3のプラグインを２日でつくる - １日目

# はじめに

_追記型でどんどんアプデしていきます。_
_用語が多くてルー大柴みたいになります。_

作業リポジトリは[こちら](https://github.com/shinichi-takahashi/eccube3-sample-plugin/tree/CategoryContent/CategoryContent)

プラグインの利用方法については[こちら](http://qiita.com/shinichi-takahashi/items/042e88ac37e635e925b2)

_2日目できました_ [こちら](http://qiita.com/shinichi-takahashi/items/edab0ded969860af58cb)

## 目的

* オーナーズストアに商品を増やしたい。
* プラグインを作る開発者に向けて、手助けになる記事にしたい。
* EC-CUBE3本体で、足りない箇所を洗い出したい。

# EC-CUBE3のプラグインを２日でつくる - 1日目

というわけで作っていきます。
まずはどんなもの作るかってところから。

## 構想

カテゴリコンテンツというEC-CUBE2系であったプラグインをver.3にあわせて作り変えようと思います。
設計というほどのものではないですが、利用するEventはおおまかには以下な感じ。

* Entity拡張：独自のものを定義
* View拡張：Symfony/kernel.response

これらを軸に必要なものを肉付けしたいと思います。

### カテゴリコンテンツとは？

カテゴリごとにデザインを変えたい！そんなあなたに！なプラグインです。
完成後に画像をアップします。

### Entity拡張

何をやるにもまずはEntityから作ります。
（作る順番は好みです）

1. Entity
2. Repository
3. Form\Type, Form\Extension
4. Twig

の順で書いていきます。

カテゴリページ( http://example.com/products/list?category_id=X )に紐づく拡張なので、
カテゴリ編集画面側からつくります。

画像の矢印の位置に入れたいと思います。
![Screen Shot 2015-07-06 at 13.56.54.png](https://qiita-image-store.s3.amazonaws.com/0/73113/4ef126f3-5e7d-0384-06a8-9893543a0bf7.png)

まずはミニマムに。カテゴリIDに紐づくtextareaをつけるだけのシンプルな拡張テーブルを用意。
MetadataのYamlも一緒に作った方が安全です。

```php
# app/Plugin/CategoryContent/Entity/CategoryContent.php
<?php

namespace Plugin\CatgeoryContent\Entity;

use Eccube\Entity\Category;

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

    private $content;

    public function setCategory(Category $Category)
    {
        $this->Category = $Category;

        return $this;
    }

    public function getCategory()
    {
        return $this->Category;
    }

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

        return $this;
    }

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

```yaml
# app/Plugin/CategoryContent/Resource/doctrine/Plugin.CategoryContent.Entity.CategoryContent.dcm.yml
Plugin\CategoryContent\Entity\CategoryContent:
    type: entity
    table: category_content
    repositoryClass: Plugin\CategoryContent\Repository\CategoryContent
    id:
        category_id:
            type: smallint
            nullable: false
            unsigned: false
            id: true
            generator:
                strategy: NONE
    fields:
        content:
            type: text
            nullable: false
        create_date:
            type: datetime
            nullable: false
        update_date:
            type: datetime
            nullable: true
    oneToOne:
        Category:
            targetEntity: Category
            joinColumn:
                name: category_id
                referencedColumnName: category_id
    lifecycleCallbacks: {  }
```

### Repositoryを用意

つづいてRepository。こちらは最初のうちはEntityで指定したClassを書いておくだけで大丈夫です。

```php
# app/Plugin/CategoryContent/Repository/CatgeoryContentRepository.php
<?php

namespace Plugin\CategoryContent\Repository;

use Doctrine\ORM\EntityRepository;

class CategoryContentRepository extends EntityRepository
{
}
```

### インストール用のファイルを定義

さて、拡張したEntityをDBに登録するために、PluginManagerとMigrationファイルを記載して、DBとの疎通テスト＋登録をおこないます。

ざっとひな形を置いておきます。

```php
<?php

namespace Plugin\CategoryContent;

use Eccube\Plugin\AbstractPluginManager;

class PluginManager extends AbstractPluginManager
{

    public function install($config, $app)
    {
        $this->migrationSchema($app, __DIR__ . '/Migration', $config['code']);
    }

    public function uninstall($config, $app)
    {
        $this->migrationSchema($app, __DIR__ . '/Migration', $config['code'], 0);
    }

    public function enable($config, $app)
    {
    }

    public function disable($config, $app)
    {
    }

    public function update($config, $app)
    {
    }
}
```

```php
<?php
# app/Plugin/CategoryContent/Migration/Version20150706204400.php

namespace DoctrineMigrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

class Version20150706204400 extends AbstractMigration
{

    public function up(Schema $schema)
    {
        $this->createDtbCategoyContentPlugin($schema);
    }

    public function down(Schema $schema)
    {
        $schema->dropTable('category_content');
    }

    protected function createDtbCategoryContentPlugin(Schema $schema)
    {
        $table = $schema->createTable("category_contnet");
        $table
            ->addColumn('category_id', 'integer', array(
                'notnull' => true,
            ))
            ->addColumn('content', 'text')
        ;
    }
}
```

こちらを用意して、tar.gzを作ったらインストールしてみましょう。

### Form\Extensionの作成

続いて、カテゴリ登録ページの、さきほどの画像のところにデータを入れたいので、 `Form\Extension` として実装していきます。
こちらも１カラム追加するだけなのでシンプルに。
`getExtendedType()` には、Extendしたいやつを指定してやりましょう。
指定したところが勝手に拡張されます。
でも親側でbindされたくないので `'mapped' => false` にしておいてあげましょう。

```php
# app/Plugin/CategoryContent/Form/Extension/CategoryContentExtension.php
<?php

namespace Plugin\CategoryContent\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;

class CategoryContentExtension extends AbstractTypeExtension
{
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder
            ->add('content', 'textarea', array(
                'lable' => 'カテゴリ別表示用コンテンツ',
                'mapped' => false,
            ))
        ;
    }

    public function getExtendedType()
    {
        return 'category';
    }
}
```

忘れないうちに `ServiceProvider` に記載しておきましょう。
忘れてたので、Repositoryも登録します。

_登録してないのに「動かない！ﾑｷｰ」ってなること結構あります。マジで。おはやめの登録を。_

```php
# app/Plugin/CategoryContent/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)
    {
        // Form/Extension
        $app['form.type.extensions'] = $app->share($app->extend('form.type.extensions', function ($extensions) {
            $extensions[] = new \Plugin\CategoryContent\Form\Extension\CategoryContentExtension();

            return $extensions;
        }));

        //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)
    {
    }
}
```
さっきから思ってますが、 `category_content` って長い。


### Viewの拡張

さて、 `{{ form_widget(form) }}` で出力してる場合は、Viewの拡張がほとんどいらないんですが、
今回はデザイナに見えなくなることを防ぐために、一個一個分解して記述しているため、追加したFormをRenderしてあげなければなりません。

`event.yml` にイベントを定義して、 `kernel.response` にイベントを知らせてあげましょう。
書き方はこんな感じです。

```yaml
# app/Plugin/CategoryContent/event.yml
eccube.event.render.admin_product_category_edit.before:
    - [onRenderAdminProductCategoryEditBefore, NORMAL]
```

**長い!!**

これで、 `admin_product_category_edit` のルーティングできたときにRenderイベントが拡張できます。
わたってくる引数は `Symfony\Component\HttpKernel\Event\FilterResponseEvent` 型です。

こいつからResponseオブジェクトを、さらにそこからHTMLのデータを引っ張りだしてきます。
`$event->getResponse()->getContent()`

そして更に、コネコネします。
コネコネの仕方はSymfony/DomCrawlerつかってもよし、DomDocument使ってもよし、replaceしてもよし。（replaceはオススメできません）

```php
<?php

namespace Plugin\CategoryContent;

use Eccube\Event\RenderEvent;
use Eccube\Event\ShoppingEvent;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\CssSelector\CssSelector;

class CategoryContent
{
    private $app;

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

    public function onRenderAdminProductCategoryEditBefore(FilterResponseEvent $event)
    {
        $app = $this->app;
        $request = $event->getRequest();
        $response = $event->getResponse();

        // DomCrawlerにHTMLを食わせる
        $html = $response->getContent();
        $crawler = new Crawler($html);

        $form = $app['form.factory']
            ->createBuilder('admin_category')
            ->getForm();
        $form->handleRequest($request);
        $twig = $app->renderView(
            'CategoryContent/Resource/template/Admin/category.twig',
            array('form' => $form->createView())
        );

        $oldHtml = $crawler
            ->filter('form')
            ->first()
            ->html()
        ;
        $newHtml = $oldHtml . $twig;

        // DomCrawlerからHTMLを吐き出す
        $html = $crawler->html();
        $html = str_replace($oldHtml, $newHtml, $html);

        $response->setContent($html);
        $event->setResponse($response);
    }

}
```

コネコネする前に適当に加工して、表示できるかどうかのテストをしておくといいかと思います。

さらに、読み込むViewをこんな感じに定義しときます。

```html
<!-- app/Plugin/CategoryContent/Resource/template/admin/category.twig -->
<br />
<div class="form-group">
    {{ form_label(form.content) }}
    <div class="col-sm-9 col-lg-10" style="margin-top: 20px;">
        {{ form_widget(form.content, { attr : { 'rows' : 15, style : 'font-size:12px' } }) }}
        {{ form_errors(form.content) }}
    </div>
</div>
```

ここまででだいたいこんな感じになりました。
この画面はBootstrapを崩してHTMLが記述されているため、
あまりいい感じじゃないですが、今回は置いておきます。

![Screen Shot 2015-07-06 at 20.28.32.png](https://qiita-image-store.s3.amazonaws.com/0/73113/8d510c4d-8fae-5dc8-1ff1-4d0f78ab45a6.png)

ここまできたら管理画面はバインドして登録するだけですね。


### 後処理ミドルウェアの追加

バインドしてデータ保管するために、後処理用のミドルウェアを使います。
`event.yml` に追記してあげます。

```yaml
eccube.event.controller.admin_product_category.after:
    - [onAdminProductCategoryEditAfter, NORMAL]
eccube.event.controller.admin_product_category_edit.after:
    - [onAdminProductCategoryEditAfter, NORMAL]
```

そして、中身はこんな感じ。
２日しかないのでソースが汚いのは勘弁してくださいm(_ _)m

```php
<?php
# app/Plugin/CategoryContent/CategoryContent.php
namespace Plugin\CategoryContent;

use Eccube\Event\RenderEvent;
use Eccube\Event\ShoppingEvent;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\CssSelector\CssSelector;

class CategoryContent
{
    private $app;

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

    public function onRenderAdminProductCategoryEditBefore(FilterResponseEvent $event)
    {
        $app = $this->app;
        $request = $event->getRequest();
        $response = $event->getResponse();
        $id = $request->attributes->get('id');

        // DomCrawlerにHTMLを食わせる
        $html = $response->getContent();
        $crawler = new Crawler($html);

        $CategoryContent = $app['category_content.repository.category_content']->find($id);
        if (is_null($CategoryContent)) {
            $CategoryContent = new \Plugin\CategoryContent\Entity\CategoryContent();
        }

        $form = $app['form.factory']
            ->createBuilder('admin_category')
            ->getForm();
        $form->get('content')->setData($CategoryContent->getContent());
        $form->handleRequest($request);

        $twig = $app->renderView(
            'CategoryContent/Resource/template/Admin/category.twig',
            array('form' => $form->createView())
        );

        $oldCrawler = $crawler
            ->filter('form')
            ->first();

        // DomCrawlerからHTMLを吐き出す
        $html = $crawler->html();
        $oldHtml = '';
        $newHtml = '';
        if (count($oldCrawler) > 0) {
            $oldHtml = $oldCrawler->html();
            $newHtml = $oldHtml . $twig;
        }

        $html = str_replace($oldHtml, $newHtml, $html);

        $response->setContent($html);
        $event->setResponse($response);
    }

    public function onAdminProductCategoryEditAfter()
    {
        $app = $this->app;
        $id = $app['request']->attributes->get('id');

        $form = $app['form.factory']
            ->createBuilder('admin_category')
            ->getForm();

        $CategoryContent = $app['category_content.repository.category_content']->find($id);
        if (is_null($CategoryContent)) {
            $CategoryContent = new \Plugin\CategoryContent\Entity\CategoryContent();
        }
        $form->get('content')->setData($CategoryContent->getContent());

        $form->handleRequest($app['request']);

        if ('POST' === $app['request']->getMethod()) {
            if ($form->isValid()) {
                $content = $form->get('content')->getData();

                $Category = $app['eccube.repository.category']->find($id);

                $CategoryContent
                    ->setCategoryId($Category->getId())
                    ->setCategory($Category)
                    ->setContent($content);

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

}
```

管理画面は荒削りですが、以上です。
２日目はフロント画面の拡張をしていきます。

_検証以外でのプラグイン拡張が初めて、かつ、今回も検証の意味合いが大きいのでソースが多少荒っぽいのはご了承ねがいますｍｍ_