ec-cube3のプラグインを作ってコンテストに応募する


### [Hamee Advent Calender](http://qiita.com/advent-calendar/2015/hamee)の5日目です。

# はじめに
ec-cubeの3が出て触りたかったので何かプラグインを作って、丁度開催中（2015/12/18 までプラグインを募集中）の[EC-CUBE PLUGIN AWARD 3.0](http://www.ec-cube.net/events/plugin_award_30/)に出してみます。

# 何を作るか
といっても、初めてだし、時間もないので、簡単でニッチなプラグインにしようと思います。
プラグインで出来ることがある程度わかりたいので、ショップ側画面と管理画面を変更するような内容で考えることにします・・・数分後・・・

## こちらのサイトでもお買い求め頂けます。プラグイン

決まりました。これを作ります。
本店サイトのページに来たお客様を本店以外のモールへ誘導する、逆サテライトサイトなプラグインです。お店からすれば本店からお客様を逃しなくないですが、お客様からすればよく利用するモールでも売っているならそっちでも買いたいんじゃないかな、って思って考えました。

# 作り方を知る
まずは、本家サイトからいろいろ辿っていきます。

- [ec-cubeのgithub](https://github.com/EC-CUBE/ec-cube)
- [ec-cube3プラグインの仕様書を読む](http://downloads.ec-cube.net/src/manual/v3/plugin.pdf)
- [EC-CUBE 3 開発ドキュメント](http://ec-cube.github.io/)
- [サンプルプラグインを眺める](https://github.com/EC-CUBE)
- [EC-CUBE3プラグイン開発初期にハマったことメモ集](http://qiita.com/tuyop1/items/f2e1d95b8c807002af22)
- 最後にindex_dev.phpにアクセスするとデバッグモードになるという情報

## プラグインの構成

```lang:ec-cube3plugin
{PluginName}
├───config.yml　       //プラグイン基本情報
├───PluginManager.php //プラグインのインストール時、更新時などの処理
├───ServiceProvider
    ├───*.php         //config.ymlで定義したServiceProviderで呼び出されるファイル
├───event.yml         //利用するフックポイント
├───{EventName}.php   //フックポイントに介入する処理
├───Migration
    ├───Version{YYYYmmddHHiiss}.php //テーブル作成や削除
    Resource
    ├───doctrine
        ├───*.dcm.yml //テーブル定義
    Form              
    ├───Type
        ├───*.php　　　//独自拡張フォームの定義
    ├───Extension
        ├───*.php　　　//独自拡張フォームの定義
```

# プラグインを作る
仕様書に沿って書いていきます。
## プラグイン基本設定
設定ファイル{[config.yml](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/config.yml?at=master&fileviewer=file-view-default)}を書きます。

```lang:yaml
name: こちらのサイトでもお買い求め頂けます。プラグイン
code: OtherSite
version: 0.0.1
service:
    - OtherSiteServiceProvider
orm.path:
    - /Resource/doctrine
migration.path:
    - /migration
event: OtherSiteEvent
```
## プラグインマネージャ
プラグインのインストール時、更新時の処理{[PluginManager.php](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/PluginManager.php?at=master&fileviewer=file-view-default)}を書いていきます。

```lang:php
<?php

namespace Plugin\OtherSite;

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)
    {
    }
}
```
## どこに処理を介入させるか
今回は商品詳細画面にモールへのリンクボタン、管理画面の商品登録・更新時にモールのURLの設定する処理を介入させる{[event.yml](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/event.yml?at=master&fileviewer=file-view-default)}を記述します。

```lang:yaml
eccube.event.render.admin_product_product_new.before:
    - [addContentOnProductEdit, NORMAL]
    - [registerOtherSite, NORMAL]
eccube.event.render.admin_product_product_edit.before:
    - [addContentOnProductEdit, NORMAL]
    - [registerOtherSite, NORMAL]
eccube.event.render.product_detail.before:
    - [showOtherSite, NORMAL]
```

また、最初に書いちゃいましたが設定ファイル{[config.yml](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/config.yml?at=master&fileviewer=file-view-default)}にeventの設定を追記します。

```lang:yaml
event: OtherSiteEvent
```

## データベース
モールのURLを保存するテーブルを作成します。今回は楽天市場、Yahooショッピング、Amazon、DeNAショッピング、ポンパレモールのURLを保存するようにします。

まずは、
entity{[Entity/OtherSite.php]
(https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Entity/?at=master)} 
と
doctrineの設定{[Resource/doctrine/Plugin.OtherSite.Entity.OtherSite.dcm.yml](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Resource/doctrine/Plugin.OtherSite.Entity.OtherSite.dcm.yml?at=master&fileviewer=file-view-default)}を書きます。

```lang:php
<?php

namespace Plugin\OtherSite\Entity;

class OtherSite extends \Eccube\Entity\AbstractEntity
{
	private $product_id;
	private $rakuten_url;
	private $yahoo_url;
	private $amazon_url;
	private $dena_url;
	private $ponpare_url;
    private $Product;

    public function getRakutenUrl()
    {
        return $this->rakuten_url;
    }

    public function setRakutenUrl($rakuten)
    {
        $this->rakuten_url = $rakuten;
        return $this;
    }

    public function getYahooUrl()
    {
        return $this->yahoo_url;
    }

    public function setYahooUrl($yahoo)
    {
        $this->yahoo_url = $yahoo;
        return $this;
    }

    public function getAmazonUrl()
    {
        return $this->amazon_url;
    }

    public function setAmazonUrl($amazon)
    {
        $this->amazon_url = $amazon;
        return $this;
    }

    public function getDenaUrl()
    {
        return $this->dena_url;
    }

    public function setDenaUrl($dena)
    {
        $this->dena_url = $dena;
        return $this;
    }


    public function getPonpareUrl()
    {
        return $this->ponpare_url;
    }

    public function setPonpareUrl($ponpare)
    {
        $this->ponpare_url = $ponpare;
        return $this;
    }


    public function getProduct()
    {
        return $this->Product;
    }
    

    public function setProduct(\Eccube\Entity\Product $Product)
    {
        $this->Product = $Product;

        return $this;
    }

    public function getProductId()
    {
        return $this->product_id;
    }

    public function setProductId($productId)
    {
        $this->product_id = $productId;

        return $this;
    }

}
```
```lang:yaml
Plugin\OtherSite\Entity\OtherSite:
    type: entity
    table: plg_other_site
    repositoryClass: Plugin\OtherSite\Repository\OtherSiteRepository
    id:
        product_id:
            type: smallint
            nullable: false
            unsigned: false
            id: true
            generator:
                strategy: NONE
    fields:
        rakuten_url:
            type: string
            default: ""
        yahoo_url:
            type: string
            default: ""
        amazon_url:
            type: string
            default: ""
        dena_url:
            type: string
            default: ""
        ponpare_url:
            type: string
            default: ""
    OneToOne:
        Product:
            targetEntity: Eccube\Entity\Product
            joinColumn:
                name: product_id
                referencedColumnName: product_id
    lifecycleCallbacks: {  }
```

次にRepository{[Repository/OtherSiteRepository.php](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Repository/?at=master)}を書きます。
今回は使わないから空定義します。

```lang:php

<?php

namespace Plugin\OtherSite\Repository;

use Doctrine\ORM\EntityRepository;

class OtherSiteRepository extends EntityRepository
{
}

```

テーブルを管理するマイグレーションファイル{[Migration/Version20151201000000.php](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Migration/Version20151201000000.php?at=master&fileviewer=file-view-default)}を書きます。

```lang:php

<?php
namespace DoctrineMigrations;

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

class Version20151201000000 extends AbstractMigration
{

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

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

    protected function createOtherSiteTable(Schema $schema)
    {
        $table = $schema->createTable("plg_other_site");
        $table->addColumn('product_id', 'integer');
        $option=array('length' => 255,'notnull' => false);
        $table->addColumn('rakuten_url', 'string', $option);
        $table->addColumn('yahoo_url', 'string', $option);
        $table->addColumn('amazon_url', 'string', $option);
        $table->addColumn('dena_url', 'string', $option);
        $table->addColumn('ponpare_url', 'string', $option);
        $table->setPrimaryKey(array('product_id'));
    }
}
```


## フォームの拡張
管理画面の商品登録画面にモールのURL設定を追加する為に{[Form/Extension/Admin/OtherSiteCollectionExtension.php](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Form/Extension/Admin/OtherSiteCollectionExtension.php?at=master&fileviewer=file-view-default)}を記述します。

```lang:php
<?php

namespace Plugin\OtherSite\Form\Extension\Admin;

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

class OtherSiteCollectionExtension extends AbstractTypeExtension
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $builder
            ->add('rakuten_url', 'text', array(
                'label' => '楽天市場URL',
                'mapped' => false,
            ))
            ->add('yahoo_url', 'text', array(
                'label' => 'YahooショッピングURL',
                'mapped' => false,
            ))
            ->add('amazon_url', 'text', array(
                'label' => 'AmazonURL',
                'mapped' => false,
            ))
            ->add('dena_url', 'text', array(
                'label' => 'DeNAショッピングURL',
                'mapped' => false,
            ))
            ->add('ponpare_url', 'text', array(
                'label' => 'ポンパレモールURL',
                'mapped' => false,
            ))
        ;
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
    }

    public function getExtendedType()
    {
        return 'admin_product';
    }

}
```

## 表示の変更
管理画面に追加するテンプレート{[Resource/template/Admin/other_site.twig](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Resource/template/Admin/other_site.twig?at=master&fileviewer=file-view-default)}と商品詳細画面に追加するテンプレート{[Resource/template/Front/other_site.twig](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/Resource/template/Front/other_site.twig?at=master&fileviewer=file-view-default)}を[twigマナー](http://twig.sensiolabs.org/)書きます。

管理画面

```lang:twig

</div>
<div class="box accordion">
<div class="box accordion form-horizontal">
    <div class="box-header toggle">
        <h3 class="box-title">
        	その他のサイト
            <svg class="cb cb-angle-down icon_down"> <use xlink:href="#cb-angle-down" /></svg>
        </h3>
    </div><!-- /.box-header -->
    <div class="box-body accpanel">
		<div class="form-group">
			<label class="col-sm-3 control-label">{{ form.rakuten_url.vars.label }}</label>
            <div class="col-sm-8 col-lg-9">
				{{ form_widget(form.rakuten_url) }}
			</div>
		</div>
		<div class="form-group">
			<label class="col-sm-3 control-label">{{ form.yahoo_url.vars.label }}</label>
            <div class="col-sm-8 col-lg-9">
				{{ form_widget(form.yahoo_url) }}
			</div>
		</div>
		<div class="form-group">
			<label class="col-sm-3 control-label">{{ form.amazon_url.vars.label }}</label>
            <div class="col-sm-8 col-lg-9">
				{{ form_widget(form.amazon_url) }}
			</div>
		</div>
		<div class="form-group">
			<label class="col-sm-3 control-label">{{ form.dena_url.vars.label }}</label>
            <div class="col-sm-8 col-lg-9">
				{{ form_widget(form.dena_url) }}
			</div>
		</div>
		<div class="form-group">
			<label class="col-sm-3 control-label">{{ form.ponpare_url.vars.label }}</label>
            <div class="col-sm-8 col-lg-9">
				{{ form_widget(form.ponpare_url) }}
			</div>
		</div>
    </div>
</div>

```

商品詳細画面

```lang:php
<div class="other_site">
    <h4>こちらのサイトでもお買い求め頂けます。</h4>
    <ul class="row">
		{% if OtherSite.rakuten_url %}<li class="col-xs-6 col-sm-4"><a href="{{ OtherSite.rakuten_url }}" target="_blank" class="btn btn-default btn-sm btn-block">楽天市場</a></li>{% endif %}
		{% if OtherSite.amazon_url %}<li class="col-xs-6 col-sm-4"><a href="{{ OtherSite.amazon_url }}" target="_blank" class="btn btn-default btn-sm btn-block">Amazon</a></li>{% endif %}
		{% if OtherSite.yahoo_url %}<li class="col-xs-6 col-sm-4"><a href="{{ OtherSite.yahoo_url }}" target="_blank" class="btn btn-default btn-sm btn-block">Yahooショッピング</a></li>{% endif %}
		{% if OtherSite.dena_url %}<li class="col-xs-6 col-sm-4"><a href="{{ OtherSite.dena_url }}" target="_blank" class="btn btn-default btn-sm btn-block">DeNAショッピング</a></li>{% endif %}
		{% if OtherSite.ponpare_url %}<li class="col-xs-6 col-sm-4"><a href="{{ OtherSite.ponpare_url }}" target="_blank" class="btn btn-default btn-sm btn-block">ポンパレモール</a></li>{% endif %}
    </ul>
</div>

```



## イベントの処理を記述する
実際ここは表示の変更やフォームの拡張やデータベースをやりながら記述しています。
[OtherSiteEvent.php](https://bitbucket.org/junya_suzuki/ec-cube-other-site-plugin/src/f3c75c97ef6d2072e408e122a97e94370768f94c/OtherSiteEvent.php?at=master&fileviewer=file-view-default)

```lang:php

<?php

namespace Plugin\OtherSite;

use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class OtherSiteEvent
{
    private $app;

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

    public function showOtherSite(FilterResponseEvent $event)
    {
        $app = $this->app;

        $id = $app['request']->attributes->get('id');
        $Product = $app['eccube.repository.product']->find($id);
        $OtherSite = $app['eccube.plugin.repository.other_site']->find($id);

        if (count($OtherSite) > 0) {
            $twig = $app->renderView(
                'OtherSite/Resource/template/Front/other_site.twig',
                array(
                    'OtherSite' => $OtherSite,
                )
            );

            $response = $event->getResponse();
            $html = $response->getContent();
            $crawler = new Crawler($html);
            $oldElement = $crawler->filter('.cart_area .btn_area');
            $oldHtml = $oldElement->html();
            $newHtml = $oldHtml.$twig;

            $html = $crawler->html();
            $html = str_replace($oldHtml, $newHtml, $html);

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

    public function registerOtherSite(FilterResponseEvent $event)
    {

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

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

		$OtherSite=$app['eccube.plugin.repository.other_site']->find($id);
					
		if (is_null($OtherSite)) {
  			$OtherSite = new \Plugin\OtherSite\Entity\OtherSite();
		}
    	$form->get('rakuten_url')->setData($OtherSite->getRakutenUrl());
    	$form->get('yahoo_url')->setData($OtherSite->getYahooUrl());
    	$form->get('amazon_url')->setData($OtherSite->getAmazonUrl());
    	$form->get('dena_url')->setData($OtherSite->getDenaUrl());
    	$form->get('ponpare_url')->setData($OtherSite->getPonpareUrl());
				
	    $form->handleRequest($app['request']);		

       if ('POST' === $app['request']->getMethod()) {
            if ($form->isValid()) {
                $Product = $app['eccube.repository.product']->find($id);
                $OtherSite
                    ->setProductId($Product->getId())
                    ->setProduct($Product)
                    ->setRakutenUrl($form->get('rakuten_url')->getData())
                    ->setYahooUrl($form->get('yahoo_url')->getData())
                    ->setAmazonUrl($form->get('amazon_url')->getData())
                    ->setDenaUrl($form->get('dena_url')->getData())
                    ->setPonpareUrl($form->get('ponpare_url')->getData())
                    ;
                $app['orm.em']->persist($OtherSite);
                $app['orm.em']->flush();
            }
        }

    }

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

        $html = $response->getContent();
        $crawler = new Crawler($html);

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

        if ($id) {
            $Product = $app['eccube.repository.product']->find($id);
        } else {
            $Product = new \Eccube\Entity\Product();
        }

        $OtherSite = $app['eccube.plugin.repository.other_site']->findOneBy(
            array(
                'product_id' => $id,
            ));
		if (isset($OtherSite)) {
        	$form->get('rakuten_url')->setData($OtherSite->getRakutenUrl());
        	$form->get('yahoo_url')->setData($OtherSite->getYahooUrl());
        	$form->get('amazon_url')->setData($OtherSite->getAmazonUrl());
        	$form->get('dena_url')->setData($OtherSite->getDenaUrl());
        	$form->get('ponpare_url')->setData($OtherSite->getPonpareUrl());
		}
        $form->handleRequest($request);

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

		$oldElement = $crawler
		    ->filter('.accordion')
		    ->last();

		if ($oldElement->count() > 0) {
		    $oldHtml = $oldElement->html();
    		$newHtml = $oldHtml.$twig;
    		$html = $crawler->html();
    		$html = str_replace($oldHtml, $newHtml, $html);
		    $response->setContent($html);
    		$event->setResponse($response);
		}
    }
}

```

## パッケージにして完成
最後にファイルを.tar.gzにまとめて完成
注：プラグインディレクトリをパッケージするのではなく、配下のファイル群をパッケージする事。

## 完成品
### 管理画面
<img width="2558" alt="スクリーンショット 2015-12-05 16.45.33.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/71312/51f09532-ac45-4fc2-da71-05024e27f1a2.jpeg">


### 商品詳細
<img width="1720" alt="スクリーンショット 2015-12-05 16.44.32.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/71312/61d41d9f-5c13-7f13-06b7-33e5703a1769.jpeg">


# EC-CUBE PLUGIN AWARD 3.0に出す
## パートナー登録をする
[EC-CUBEパートナー登録](https://www.ec-cube.net/entry/partner_kiyaku.php)
登録したんですが、組織名、組織URL、組織説明はコンテスト応募の敷居を高くしてる気がします。個人で参加出来る事はもっと明示した方がいいのでは？

## プラグイン申請準備
[EC-CUBEプラグイン申請マニュアル](https://www.ec-cube.net/document/GL02_plugin_apply.pdf)を参考にロゴとか説明とか入力して申請完了。ありがとうございました。

# まだまだ全然理解してないけど他に何作ろうかなぁ :trollface: