2020年5月2日土曜日

EC-CUBE4系 オーナーズストアからインストールしたプラグインの独自アップデートを行う


SYSTEM_KDです。

久々のブログ更新です。

今回は、誰向けなのか不明な、EC-CUBE4系でオーナーズストアからインストールしたプラグインを独自アップデートを行う方法です。


オーナーズストアからインストールしたプラグインのアップデートは簡単には行えない


オーナーズストアからプラグインのインストールを行うと、基本的にはプラグインのアップデートはオーナーズストア経由でしか行なえません。

特に問題はないのですが、バグってて今すぐ修正できるパッチを用意しておきたい場合や、独自のカスタマイズを適用したいと言う場合には、差分ファイルを手動適用するということでしか対応できません。

(もしや、ec-cube.coは手動適用もできないんじゃないかな・・)


EC-CUBE3系までのプラグインは、オーナーズストアのマイページからダウンロードできるので、独自プラグインとしてインストールすれば、そっち側でアップデートが可能。 ってそんな使い方はやらないですかね。


まぁ、そんなこんなで4系はアップデートしたいときの回避策がないという不安を解消するために、アップデートする策を考えました。

その策とは・・・


独自プラグインをインストールし、そのプラグインでオーナーズストアからインストールしたプラグインをアップデート


ドン!(大した策じゃない


4系でも、独自プラグインのインストールできるので、そっち側から「オーナーズストアのプラグイン」の方を更新してしまうという作戦です。


と言っても、既にオーナーズストアでインストールされているものを、独自プラグインでインストールしようとしても、それは行えません。


流れとしては、アップデートしたいプラグインの、アップデート後のプラグイン(tar.gz)を含めたプラグインを、独自プラグインとして用意。
そのプラグインをインストールし、有効化する際の処理で、(保持しているプラグインファイルを用いて)独自アップデートしてしまうというものです。

(説明下手くそ)


まぁ、こんなイメージ。



で、コードの方は、

namespace Plugin\PlgPatch; // 環境に合わせて変更


use Eccube\Entity\Plugin;
use Eccube\Exception\PluginException;
use Eccube\Plugin\AbstractPluginManager;
use Eccube\Repository\PluginRepository;
use Eccube\Service\PluginService;
use Eccube\Util\CacheUtil;
use Eccube\Util\StringUtil;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

class PluginManager extends AbstractPluginManager
{

    /** @var PluginRepository */
    protected $pluginRepository;

    /** @var CacheUtil */
    protected $cacheUtil;

    /** @var PluginService */
    protected $pluginService;

    private $upFileName;

    private $plgCode;

    private const PLG_PATH = __DIR__ . '/Resource/upfile/';

    /**
     * @param array $meta
     * @param ContainerInterface $container
     * @throws PluginException
     */
    public function enable(array $meta, ContainerInterface $container)
    {

        log_info('[PlgPatch]パッチ適用開始');

        $this->initialize($container);

        log_info('[PlgPatch]更新対象プラグインの存在チェック開始');

        $Plugin = $this->getPlugin();

        log_info('[PlgPatch]更新対象プラグインの存在チェック終了');

        // プラグインの手動アップデート
        $this->plgManualUpdate($Plugin);

        log_info('[PlgPatch]パッチ適用完了');
    }

    /**
     * @param ContainerInterface $container
     */
    private function initialize(ContainerInterface $container)
    {
        $this->pluginRepository = $container->get(PluginRepository::class);
        $this->cacheUtil = $container->get(CacheUtil::class);
        $this->pluginService = $container->get(PluginService::class);

        $finder = Finder::create()
            ->in(self::PLG_PATH)
            ->files();

        /** @var \SplFileInfo $item */
        foreach ($finder as $item) {
            $this->upFileName = $item->getFilename();

            $fileName = explode(".", $item->getFilename());
            $this->plgCode = $fileName[0];
        }
    }

    /**
     * @param string $plgCode
     * @return Plugin
     * @throws PluginException
     */
    private function getPlugin($plgCode = "")
    {

        if (empty($plgCode)) {
            $plgCode = $this->plgCode;
        }

        /** @var Plugin $Plugin */
        $Plugin = $this->pluginRepository->findOneBy(['code' => $plgCode]);

        if (!$Plugin) {
            log_error("[PlgPatch]更新対象プラグインがインストールされていません。");
            throw new PluginException("[PlgPatch]更新対象プラグインがインストールされていません。");
        }

        return $Plugin;
    }

    /**
     * @param Plugin $Plugin
     * @param string $upFileName
     * @throws PluginException
     */
    private function plgManualUpdate(Plugin $Plugin, $upFileName = "")
    {

        $fs = new Filesystem();

        if ($upFileName == "") {
            $upFileName = $this->upFileName;
        }

        $upFilePath = self::PLG_PATH . $upFileName;

        /** @var CacheUtil $cacheUtil */
        $cacheUtil = $this->cacheUtil;

        /** @var PluginService $pluginService */
        $pluginService = $this->pluginService;

        // ファイル配置
        $tmpDir = null;
        try {
            $cacheUtil->clearCache();

            $tmpDir = $pluginService->createTempDir();
            $tmpFile = sha1(StringUtil::random(32)) . '.tar.gz';

            log_info('[PlgPatch]プラグインファイルコピー開始', [
                'origin' => $upFilePath,
                'target' => $tmpDir . '/' . $tmpFile,
            ]);

            $fs->copy($upFilePath, $tmpDir . '/' . $tmpFile, true);

            log_info('[PlgPatch]プラグインファイルコピー完了');

            log_info('[PlgPatch]Update', [$Plugin]);

            $pluginService->update($Plugin, $tmpDir . '/' . $tmpFile);

            log_info('[PlgPatch]一時ディレクトリから削除', [$tmpDir]);

            $fs->remove($tmpDir);

            log_info('[PlgPatch]一時ディレクトリから削除完了');

            return;

        } catch (PluginException $e) {
            if (!empty($tmpDir) && file_exists($tmpDir)) {
                $fs = new Filesystem();
                $fs->remove($tmpDir);
            }
            $message = $e->getMessage();
            log_error('[PlgPatch]' . $message);

            throw $e;
        } catch (\Exception $er) {
            // Catch composer install error | Other error
            if (!empty($tmpDir) && file_exists($tmpDir)) {
                $fs = new Filesystem();
                $fs->remove($tmpDir);
            }
            log_error('plugin install failed.', ['original-message' => $er->getMessage()]);
            throw $er;
        }
    }
}

主要部分を全部はっつけてみた。

(わりと多かった・・・githubにするべきだったか)



普通にプラグインを作成する要領で、composer.json を用意して、上記コードをPluginManager.phpとして配置。

Resource/upfile にアップデートを行いたいプラグインを[プラグインコード].tar.tz のファイル名で配置すればOK.


独自プラグインとして、インストールした後、有効化を行うと Resource/upfile に格納したプラグインでアップデートされます。
(※複数は未対応)



注意点

上記で、そこそこ上手く更新できると思いますが、注意点がいくつかあります。

1. ファイルは上書きで配置される

もとのプラグインファイルに対して、更新用プラグインが上書きされます。
ファイルの置き換えのみであれば、問題ないのですがファイルを削除している場合は消えてくれないので注意。


2. スキーマがアップデートされない

次期バージョン(4.0.4)で解決されるはずですが、4.0.3までは独自プラグインのアップデートでは、スキーマ更新されないので、テーブルに変更を行った場合このアップデートだけでは反映されません。

残念・・・

といいつつ、これは対応策があります。

更新用に独自プラグインとしてインストールしたプラグインをアンインストールするだけです。

プラグインのアンインストールを実施すると、スキーマアップデートが流れてくれるので、その処理の巻き添え(良い意味で)でアップデートされます。

更新用プラグインは、アップデートが終わると必要ないですし、アンインストールまでが一連の更新処理ということで。


3.バージョンは変えない方が良い

アップデート対象のプラグインのバージョンを変えてしまうと、次のアップデートできちんとアップデートされない可能性があるので、アップデート対象プラグインのバージョン番号は変えずに、プラグイン名に、変更版である旨を追記するのが良いです。


以上。

2019年8月3日土曜日

Pluginで追加したEntityを別Pluginにて拡張

SYSTEM_KDです。




EC-CUBE4でのプラグイン開発時メモです。


EC-CUBE4系では、traitを用いてEntityを拡張できるようにEntityの拡張機構が用意されています。


※Entity拡張機構


これによって、プラグインから本体のEntityを拡張することができるのですが、プラグインで追加したEntityを別のプラグインから拡張しようとして、微妙にハマってしまったとメモになります。



プラグインで追加したEntityをプラグインで拡張


やり方としては、本体のEntityを拡張するときと同様。
アノテーションで適用するEntityを指定する際に、プラグインのEntityを指定するだけ。
/**  
 * Trait LabTrait 
 * @Eccube\EntityExtension("Plugin\TargetPlugin\Entity\TargetClass")  
 */
trait LabTrait  
{  
  public function getDummy()  
  {
    return "xxx";  
  }  
}

ただ、拡張される側のEntityを普通に用意しただけだと、Entity重複してるよと怒られるので、本体側のEntityと同じように、クラスが存在してるかチェックで囲むようにします。

※これを実施しておかないと、proxy生成は成功するけど生成されたファイルを削除しないと、それ以降どうしようもなくなってしまうので注意

if (!class_exists('\Plugin\TargetPlugin\Entity\TargetClass')) {
    /**
     * TargetClass
     *
     * [略]
     */
    class TargetClass extends \Eccube\Entity\AbstractEntity
    {
      ・・・
    }
}

これでOKっと思ったけど、ここがハマりポイントでした。

これだと、bin/console eccube:generate:proxies を実行した際にエラーが発生してしまいました。

プラグインのEntityは拡張できないのかなと思いつつ、EC-CUBEの拡張機構部分のコードを追ってみると、}の後に改行が欲しそうな雰囲気だったので、付け足してみたら解決しました。

(実は環境の問題とかって可能性もあるのかな・・・)

if (!class_exists('\Plugin\TargetPlugin\Entity\TargetClass')) {
    /**
     * TargetClass
     *
     * [略]
     */
    class TargetClass extends \Eccube\Entity\AbstractEntity
    {
      ・・・
    }
}
// ← 最後の空行部分

ということで、Pluginで追加したEntityを別Pluginにて拡張する際のメモでした。

2019年5月23日木曜日

EC-CUBE4でプラグインからのマイグレーションでEntityManager・Entityを使う方法

SYSTEM_KDです。


今回のブログから、StackEdit を利用してMarkdownで書いてみています。
(いや、何の宣言?


久々の投稿ですが、内容はEC-CUBE4でのマイグレーションについてです。


ほぼメモです。



EC-CUBE4でのプラグインからマイグレーション


EC-CUBE3では、プラグインでテーブルの拡張・データ追加を行う際はPluginMigrationで行いますが、
4系ではテーブルの拡張は「doctrine:schema:update」での拡張が推奨されてます。

(まぁ、プラグインからのテーブル拡張は、プラグイン機構部分が良しなにしてくれるのであまり気にしなくてOK)


データの追加を行う際も、PluginMigrationから行えます。

AbstractPluginManager を継承して enable() 関数にてinsertするなり、$container(ContainerInterface) からEntityManagerを利用するなりしてデータ追加が行えます。


ただ、この方法だと再度 enable()が操作した際にinsertしないようにチェックする必要があります。

プラグインをバージョンアップして、追加するデータが増えた際・既に追加しているデータを変更する際など、管理が面倒くさい。


振り返らずに、前に進みたいです。


ということで、3系と同様にマイグレーションファイルを用意して、追加するデータを管理したいと思います。


必要なファイル
(プラグイン用ディレクトリの中)
  • PluginManager.php
  • DoctrineMigrations/Version2019XXXXXXXXXX.php


    VersionXXXのファイルは、下記コマンドで生成
    (生成できたらプラグインの中に持ってくる)
bin/console doctrine:migrations:generate

PluginManager.php は、Eccube\Plugin\AbstractPluginManagerを継承する。

AbstractPluginManagerにある、migrationメソッドを呼べば3系の時みたいにバージョン管理しながらデータ追加が行える。


有効にした際にデータ追加したい場合
public function enable(array $meta, ContainerInterface $container)  
{  
    $this->migration($meta['code'], $container);
}

逆にアンインストールで削除する場合
public function uninstall(array $meta, ContainerInterface $container)  
{  
    $this->migration($meta['code'], $container, 0);  
}


あとは、マイグレーションファイル内の up() 関数で、`addSql`すれば良い。
※アンインストール時は、down()

(色々とサンプルが転がっているのでざっくりで m(_ _)m


でも、これだとEntityManager使えない。Entityが使えない。。使いたい。。


で、本題のEntityManagerを使ってのマイグレーションです。

マイグレーションファイルで、ContainerAwareInterfaceをimplementsし、
use ContainerAwareTraitを差し込んであげれば、ContainerInterfaceを使えるようになると見せかけて、ならない。

bin/console doctrine:migrations:migrate

で、app/DoctrineMigrations にあるマイグレーションファイルは良い感じになりますが、プラグインの方はダメっぽい。

やり方が悪いだけだったりして・・・^^;


解決方法
Eccube\Plugin\AbstractPluginManagermigration 関数をベースにちょっと書き換えます。

PluginManager .php
class PluginManager extends AbstractPluginManager  
{
 /**  
  * @param array $meta  
  * @param ContainerInterface $container  
  * @throws MigrationException  
  */
 public function uninstall(array $meta, ContainerInterface $container)  
 {  
     $this->pluginMigration($meta, $container, 0);  
 }  
   
 /**  
  * * @param array $meta  
  * @param ContainerInterface $container  
  * @throws MigrationException  
  */
 public function enable(array $meta, ContainerInterface $container)  
 {  
     $this->pluginMigration($meta, $container);  
 }

 protected function pluginMigration(array $meta, ContainerInterface $container, $version = null)  
 {
     $migrationFilePath = __DIR__ . '/DoctrineMigrations';  
     $connection = $container->get('database_connection');  
   
     $pluginCode = $meta['code'];  
   
     $config = new Configuration($connection);  
     $config->setMigrationsNamespace('\Plugin\\' . $pluginCode . '\DoctrineMigrations');  
     $config->setMigrationsDirectory($migrationFilePath);  
     $config->registerMigrationsFromDirectory($migrationFilePath);  
     $config->setMigrationsTableName(self::MIGRATION_TABLE_PREFIX . $pluginCode);  
   
     /** @var Version $objVersion */  
     foreach ($config->getMigrations() as $objVersion) {  
         $versionMigration = $objVersion->getMigration();  

         if ($versionMigration instanceof ContainerAwareInterface) {  
             $versionMigration->setContainer($container);  
         }  
     }
   
     $migration = new Migration($config);  
     $migration->migrate($version, false);  
 }
}

間違え探しみたいな微妙な違いですが、、
キモとなる部分はこの部分になります。

ContainerInterfaceをVersionXXX側に渡してあげるようにします。
/** @var Version $objVersion */  
foreach ($config->getMigrations() as $objVersion) {  
    $versionMigration = $objVersion->getMigration();  

    if ($versionMigration instanceof ContainerAwareInterface) {  
        $versionMigration->setContainer($container);  
    }  
}


これで無事にマイグレーションをバージョン管理しながら、EntityManager・Entityを使うことができます。


final class VersionXXX extends AbstractMigration implements ContainerAwareInterface  
{
    use ContainerAwareTrait;

    public function up(Schema $schema): void  
    {  
        /** @var EntityManagerInterface $entityManager */  
        $entityManager = $this->container->get('doctrine.orm.entity_manager');  
   
        $ProductRepository = $entityManager->getRepository(Product::class);
    }  
   
    public function down(Schema $schema): void  
    {  
   
    }
}


まとめ


bin/console でマイグレーションする場合は、VersionXXXの方だけで良いけど、
EC-CUBE4のプラグインでマイグレーションする際は、PluginMigrationでContainerInterfaceを渡してあげる必要がある。

2019年2月3日日曜日

Azure (App Service) に EC-CUBE 4 をインストール


SYSTEM_KD です。

自分用のEC-CUBE3 デモ環境を Azure に構築していますが、EC-CUBE4系も欲しいなということで、今更ながらEC-CUBE4 のデモ環境を Azureに構築したいと思います。

ちなみに、なぜAzureかというと、「」で「便利」で「安い」からです。

App Service を利用すれば、マネージドな環境なのでサーバの設定を気にすることもなく楽。
SSL証明書も付いてる(もちろん独自ドメインでな無く、そもそもLet’s Encryptを使えば気にしなくても良いというのはありますが・・まぁ環境作ったら付いてるので便利)
安いのは、そのままです。無料で使えます。


App Service 作成


では、環境作成します。

Azure にアクセスして、左側のメニューから App Service を選択します。


image

App Service のページが表示されるので、「+追加」から「Web Apps」を選択します。

作成をクリックして、「アプリ名」に好きな名称(ドメイン名になるので、他の人とかぶるとダメ)を指定。

リソースグループは「新規作成」を選択。(別に既存があるなら既存でも良い。自由に)

OS は「Windows」を選択(Linuxを選ぶと有料)

公開で、「コード」を選択。(Docker イメージは有料)

App Service プラン/選択で、新規作成から、場所は「Japan」で、プランは、「開発/テスト」の「F1」を選択。

image


(しばらく見ないうちに、UIめちゃめちゃ変わっている。。^^; 


「作成」で環境用意完了!!


App Service で先程作成したものを選択して、概要の「URL」をクリックすると、デフォページが表示されるかと思います。


EC-CUBE4 をインストール


環境が作成できましたので、インストール作業をすすめていきます。


ソースコードをアップしたいのですが、FTP経由でアップすると時間がかかるので、Git でアップしたいと思います。


メニューから、「デプロイセンター」を選択して、「Local Git」を選びます。
※ここらへんも前と変わっている・・・。


ひとまず、git clone します。


clone できたら、そこに EC-CUBE4のソースを配置して、コミットして、push します。


※ディレクトリを作成して、その中にソースを置くと Azure 側の「仮想アプリケーションとディレクトリ」の設定を調整する必要があるので、とくにディレクトリは作成せずそのままソースコードを配置


これで配置は完了。


さくっとインストール。


のはずが、微妙に罠がありました。


まず、web.config でエラーが発生してインストールが表示されない。

<add segment=”bin” />

を削除して対応。


ついでに、 git 管理から、 bin ディレクトリを除外。


app/Plugin ディレクトリが空で、git 管理から外れるため、.gitkeep を追加。

同じく、app/Customize/Controller と app/Customize/Entity にも、.gitkeep を追加


終わりと思いきや、まだあります。。

app/template/admin
app/template/default
app/template/user_data
app/proxyentity
app/PluginData
src/Eccube/Resource/doctrine/migration

にも同様に、.gitkeep 追加


再度 push して、インストールにトライ!


重い。。



image


ようやく、インストール画面。


DBは、MySQL In App を使います。


Azureのコンソールで、

D:\home\data\mysql
に移動すると


MYSQLCONNSTR_localdb.txt があるので、表示すると接続情報が書いてますので、DBへの接続情報はそれを利用します。


image


なんとか、インストール完了。


git 経由でアップしたことによって、色々とハマりましたが、4系用の検証環境ができました。


ただ、Azure 無料プランで無理やり動かすのは厳しいようで、最初の表示はかなり時間を必要とします。
(キャッシュが生成されれば、それなりには動きます)


2019年1月15日火曜日

wsl + Docker for Windows でdocker-compose を動作させる際の Volume設定について


SYSTEM_KDです。
年初はブログ更新を頑張るけど、すぐに力尽きます。。^^;

docker-compose.yml の Volume 設定について

前回、環境をCygwinから wsl へ変更したという記事を書きましたが、docker-compose up してみると、volume部分で問題が発生することが判明しました。
ディレクトリは相対パスで指定していたので、問題ないだろうと思っていたのですが甘かったです。

※wsl + Docker for Windows は、tcp://localhost:2375 経由で動かすようにしてます。

具体的には、docker-compose up 自体は成功するのですが、windows側のマウントが行えていない(ディレクトリ空っぽ)状態になっていました。

調べてみると、wsl 側でホストのドライブは 「/mnt/ドライブ」 という感じで、マウントされているために上手くマウントできていない状態とのことでした。

解決策としては、設定ファイルを追加してマウントの場所を変えるということでした。
設定ファイルの追加場所は、
/etc/
で、ファイル名は
wsl.conf
になります。
中身は、
[automount]
enabled = true
root = /
options = "metadata,umask=22,fmask=11"
mountFsTab = false

こんな感じです。
マウントの場所を/mnt から / に変更している状態となります。

設定ができたら、wsl を一旦終了させます。

これで、/c/ とか /d/ とかでアクセスできるようになり、上手くマウントできるようになるかと思います。

wsl-terminal が起動しない

上記でマウント問題が解決させ、wsl を起動させたところ、起動しないという問題発生。

設定ファイル追加を、wsl-terminal 以外から実施していたので気づきませんでしたが、wsl-terminalから実施してたら、終了させた後に起動できなくなるかと思います。

ちょっと焦りました ^^;

おそらく /mnt/ 配下にドライブがマウントされていること前提で動作しているのだろう(勝手な憶測)ということで、他のターミナルとか、コマンドプロンプト経由でubuntuを起動して、ちょろっと変更します。

変更といっても、/mnt/ドライブ から /ドライブ にシンボリックリンクをはるだけです。

ln -s /c /mnt/c

Cドライブだとこんな感じですね。

これで、wsl-terminal も起動できるようになります。

以上、wsl + Docker for Windows でdocker-compose を動作させる際の Volume設定について でした。