あなたのお役に立ちたいわんど。

CS-Cartでアドオンを作ってみたい!噛み砕いて整理してみる。

こんにちは
最近PHPのぞうさんのぬいぐるみが欲しくなってきたyoseiです。

Andplusに入社して半年、いくつかのCS-Cartアドオン開発に関わらせていただくなかで、自分なりの方法がまとまり初めてきたので、ここらへんで整理もかねてそれを一旦記事に書き起こしてみようと思いました。

CS-Cart公式ドキュメントにも、もちろんアドオンの作り方はまとまっていますが、
こんなアドオンを作ってみたい!と思い立った人が手軽に学べる入門編とはちょっと役割が違います。
また、思い立ってアドオンを用いたカスタマイズに手をつけてみたとき、意外に必要になるのがCS-Cartのディレクトリ構造の知識です。前提知識として持っていないとちょっとつまづきます。

ということで設定ファイルの細かいプロパティ設定の説明などは、一旦公式さんにおまかせして、
今回は、初めてアドオンを作る人が具体的にどのような手順でアドオン開発にアプローチをしていくべきかに焦点を絞って、ディレクトリ構造の解説も踏まえつつ説明していきたいと思います。

やっていく!

*この記事は2021/11/09 CS-Cart v4.13.2時の情報です

今回アドオンで実装したい機能


今回は管理画面のプロフィール設定画面に新しく”管理者メモ”フィールドを設けて、値をDBに保存することを目標としてみましょう。
カスタマイズ箇所

CS-Cartの画面は、テンプレートとコントローラーという主に2つの要素によって機能しています。

テンプレートとは、サイトの骨組みであり、ものすごく説明を端折ると変数を扱えるHTMLのようなものです。
コントローラーとは、ユーザーが入力した値やDBの値を処理して、その値をテンプレートの変数に渡す役割をしているものです。

ではまず、このページはどのテンプレートとコントローラーを読み込んでいるのでしょうか。
それはURLのdispatchを参照することでわかります。今回のプロフィール設定画面は

https://http://localhost/test/admin.php?dispatch=profiles.update&user_id=XXX&user_type=X

といった感じなので
コントローラー:profile.php
テンプレート:update.tpl
となります。

なので、このファイル名でファイル検索をかけていくう。
…のですが、同名のファイル名が複数ヒットすることがあります。
目的のファイルはどれであるか特定しないと始まりません。
そこで、CS-cartのディレクトリ構造の知識が必要となってくる訳です。

下にCS-Cartのディレクトリ構造の一部を示します。
*ここで出てくるバックエンドは管理画面、フロントはショップ側と解釈してください。

【第1〜6階層:CS-Cart本体】
app
   ┣ addons...(あとでつかうよ)
   ┣ controllers
      ┣ backend....(バックエンドのコントローラー)
      ┣ common.....(フロント、バックエンド共通のコントローラー)
      ┣ frontend...(フロントのコントローラー)

design
   ┣ backend...(バックエンドのデザインフォルダ)
   ┃  ┣ templates...(バックエンドのテンプレート)
   ┃     ┣ views.......(ここは一旦無視)
   ┃        ┣ profiles......(バックエンドのテンプレートのカテゴリ)
   ┣ themes....(フロントのテーマフォルダ)
      ┣ resposive...(フロント:レスポンシブテーマのデザインフォルダ)
      ┃  ┣ templates...(フロント:レスポンシブテーマのテンプレート)
      ┣ bright_theme(フロント:ブライトテーマなどの他のデザインフォルダ)

ということで、今回のカスタマイズ箇所はバックエンドなので、

コントローラー: profile.phpのパスは

app/controllers/backend/profiles.php

テンプレート: update.tplのパスは

design/backend/templates/views/profiles/update.tpl

*下2層の/profiles/update.tplという部分は、”dispatch=profiles.update”と対応しています。

上のディレクトリ図でviewsという部分の説明を省きましたが、今回は同名ファイルから目当てのファイルを探すことが目的なので、
そのテンプレートが「バックエンドのものなのか、フロントのものなのか」、「パスはdispatchと対応しているか」を確かめるだけで特定が可能だと思います。
viewって具体的にどんな部品が入ってるとこ?っていうのはカスタマイズを繰り返すなかで自然と分かってきます。

これでファイルは特定できました。ですが、

「なるほど。じゃあこのファイルをもりもりカスタマイズしてけばいいわけね。」

と思ったそこのあなた。
あかん。
これらのファイルを直接書き換えると
CS-Cartアップデート時にファイルがまるごと上書きされて、カスタマイズが消失するという恐ろしいことがおきます。
また、現状組み終わっている部分を触りまくることで、誤って通常の機能まで破壊するとこれまたひじょーに困る。

そこでこの直接のファイル改変をなるべく避けてCS-Cartをカスタマイズする方法。
それがなにを隠そうフックシステムなのです。

テンプレートフックとPrePostコントローラー

CS-CartにはHook(フック)という機能が用意されています。
これは文字通り既存の処理に新しい処理を”ひっかける”方法です。
まずはテンプレートフックについて解説していきましょう。

テンプレートフック

まずはテンプレートから。
今回、管理画面のお客様編集画面のここに”管理者メモ”フィールドを設けたいわけです。
カスタマイズ箇所
なので、先程特定したupdate.tplというテンプレートから、書き足したい箇所はどこらへんか。

今回、ユーザーのタイプの条件分岐などによって表に表示されていない要素もありますが、
おおよそ54行あたりが目当ての場所だと思います。

    <div id="content_general">
        {hook name="profiles:general_content"}
            {include file="views/profiles/components/profiles_account.tpl"}

            {if ("ULTIMATE"|fn_allowed_for || $user_type == "V") && $id != $auth.user_id}

                {$zero_company_id_name_lang_var = false}
                {if "ULTIMATE"|fn_allowed_for && $user_type|fn_check_user_type_admin_area}
                    {$zero_company_id_name_lang_var = 'all_vendors'}
                {/if}

                {include file="views/companies/components/company_field.tpl"
                    name="user_data[company_id]"
                    id="user_data_company_id"
                    selected=$user_data.company_id
                    zero_company_id_name_lang_var=$zero_company_id_name_lang_var
                    disable_company_picker=$hide_inputs
                }

            {else}
                <input type="hidden" name="user_data[company_id]" value="{$user_data.company_id|default:0}">
            {/if}
        {/hook}
        <!-- ここ -->
        {include file="views/profiles/components/profile_fields.tpl" section="C" title=__("contact_information")}

ここで重要なのが、その真上で閉じられている{hook}という部分。これこそがテンプレートフック。
今回は32行で定義されているように、profilesというセクションのgeneral_contentという特有の名前を持ったフックです。

テンプレートフックを用いた処理の引っ掛け方は3種類。
・ 1つ目がpreフック。{hook}の直前に処理を挿入します。
・ 2つ目がpostフック。{/hook}の直後に処理を挿入します。
・ 3つ目がoverride。{hook}{/hook}をまるごと上書きます。
これらのルールに則して、処理を外部ファイルに記述していきます。

前提として、CS-Cartはアップデートでファイルの構造や処理が調整されても、{hook}の名前や大方の位置は引き継がれると考えて良いです。
なので、たとえバージョンアップしてフックがそのまま動かなくても、バージョンアップ後の構造や処理を参照しながら微調整すれば、バージョンアップにも対応可能というわけです。
元の処理に手を加えなくて済むという点でフックシステムは本当にありがてえ。

今回追記したい54行目は、まさにpostフックの位置なので、今回はテンプレートpostフックを使っていきます。
それでは、具体的にどうアドオンの中にhookを書いてあげればいいかを説明していきましょう。

結論から先言うと、この場合

admin_memo(アドオンのフォルダ)/design/backend/templates/addons/admin_memo(アドオンID)/hooks/profiles/general_content.post.tpl

という場所にファイルに、内容を記述することで、それがそのまま先程のpostフックとして認識され、レンダリング時に表にでてきます。
結論から先に言いすぎた。。。
理解するために先程のCS-Cartのディレクトリ構造を再び引っ張り出しましょう。

【第1〜5階層:CS-Cart本体】
design
   ┣ backend...(バックエンドのデザインフォルダ)
      ┣ templates...(バックエンドのテンプレートが入ってるとこ)
         ┣ addons
            ┣ XXXXXXXXXX(アドオンID)...(既存のアドオンの名前)

これがアドオンインストール前の状態。

アドオンをインストールするとき、CS-Cartくんは、アドオンフォルダのディレクトリ構造をCS-Cartのディレクトリ構造と照らしわせながら、同じ位置にファイルをコピーしてきます。
つまり、
アドオンインストール時、アドオンフォルダの

admin_memo(アドオンのフォルダ)/design/backend/templates/addons/admin_memo(アドオンID)/hooks/profiles/general_content.post.tpl

というパスをCS-Cartが認識したとき、
/design/backend/templates/addons/までは、すでにCS−Cartに存在しているので、そこまでは辿り、それ以下をコピーします。
つまり、アドオンインストール後の状態は

【第1〜8階層:CS-Cart本体】
design
   ┣ backend...(バックエンドのデザインフォルダ)
      ┣ templates...(バックエンドのテンプレートが入ってるとこ)
         ┣ addons......(アドオンがたくさん入っているところ)
            ┣ admin_memo(アドオンID)...(アドオンの名前)
            ┃  ┣ hooks
            ┃     ┣ profiles................(フックのセクション名)
            ┃        ┣ general_content.post.tpl...(フック名を冠したファイル)
            ┣ XXXXXXXXXX(アドオンID)...(既存のアドオンの名前)

ということになります。

CS-Cartくんは、もとのupdate.tplを読むとき、{hook}の場所に差し掛かると、一旦立ち止まって、ここにだれかが処理をひっかけてないかな〜と探しに行くのですが、
探しに行く場所を教えて上げるために、このようなディレクトリ階層で導いてあげます。

ちなみに、preフックの場合はファイル名をgeneral_content.pre.tplに
オーバーライドしたい場合は、general_content.override.tplに変えるだけです!

今回のgeneral_content.post.tplの中身はこんな感じ。

<div class="control-group">
    <label for="remarks" class="control-label">{__("original_admin_memo")}</label>
    <div class="controls">
        <textarea class="input-large user-success" id="admin_memo" name="admin_memo" cols="32" rows="3"
            aria-invalid="false">{$admin_memo}</textarea>
    </div>
</div>

1から書くというよりも、同じ機能をもった別のところから引っ張ってきて手を加えるのが良いでしょう。
この場合ついてませんが、label に class=”cm-required” があるとフィールドが必須項目になるなど、
CS-Cart標準のクラスはスタイルだけでなく、js処理と紐付いていたりするので、そこらへんも利用しながらいきましょう。

PrePostコントローラー


次はこのフィールドに入力されたデータをDBに保存するために、コントローラーの処理をかかなければいけませんね。
そしてこのコントローラーにもフックと似た機能としてPrePostコントローラーがあります。
テンプレートフックが分かれば要領は同じです。

今回の特定したコントローラーはprofiles.phpでした。
この中で、プロフィールが保存されたときに動く処理を探しましょう。
これを探すポイントはコントローラーmodeです。話すとながくなるので、詳しい解説は公式ドキュメントで

「変更内容を保存」ボタンを開発者ツールで覗くと data-ca-dispatch=”dispatch[profiles.update]” とあります。これが保存処理時のdispatchです。

つまり、この場合のコントローラーmodeは”update”ということになります。
今回でいうと、474行目ですね。

} elseif ($mode == 'update' || $mode == 'add') {

ですが、ここに追加の処理を書いていけばいいかというと、それがあかんという話でした。
かといって、先程のようなテンプレートのようなhook名などはありません。

ではどうhookするのでしょうか?結論はこうです。

admin_memo(アドオンのフォルダ)/app/addons/admin_memo(アドオンID)/controllers/backend/profiles.pre.php

インストール前のCS-Cartのディレクトリはこうなので、

【第1〜4階層:CS-Cart本体】
app
   ┣ addons
   ┃  ┣ XXXXXXXXXX(アドオンID)...(既存のアドオンの名前)
   ┣ controllers
      ┣ backend....(バックエンドのコントローラーはここ)
      ┃  ┣ profiles.php...(もとのコントローラー)
      ┣ common.....(フロント、バック共通のコントローラーはここ)
      ┣ frontend...(バックエンドのコントローラーはここ)

インストール後のCS-Cartのディレクトリはこうなります。

【第1〜6階層:CS-Cart本体】
app
   ┣ addons
   ┃  ┣ admin_memo(アドオンID)...(アドオンの名前)
   ┃  ┃  ┣ controllers
   ┃  ┃     ┣ backend
   ┃  ┃        ┣ profiles.pre.php...(もとのコントローラーの名前を冠したファイル)
   ┃  ┣ XXXXXXXXXX(アドオンID)...(既存のアドオンの名前)
   ┣ controllers
      ┣ backend....(バックエンドのコントローラーはここ)
      ┃  ┣ profiles.php...(もとのコントローラー)
      ┣ common.....(フロント、バック共通のコントローラーはここ)
      ┣ frontend...(バックエンドのコントローラーはここ)

基本的な考え方はテンプレートフックと同じですが、テンプレートフックのようなセクション名がない影響で、階層が少しシンプルになった感じですね。
これによって、preコントローラーであるprofiles.pre.phpはprofiles.phpが処理される直前に処理されます。

今回のprofile.pre.phpの中身はこんな感じです。

<?php
 
use Tygh\Tygh;
 
if (!defined('BOOTSTRAP')) {
    die('Access denied');
}
 
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($mode == 'update' || $mode == 'add') {
        $user_id = $_REQUEST['user_id'];
        $admin_memo = $_REQUEST['admin_memo'];
        db_query('UPDATE ?:user_profiles SET admin_memo = ?s WHERE user_id = ?i', $admin_memo, $user_id);
    }
}
 
if ($mode == 'update' || $mode == 'add') {
    $user_id = $_REQUEST['user_id'];
    $admin_memo = db_get_field('SELECT admin_memo FROM ?:user_profiles WHERE user_id = ?s', $user_id);
    Tygh::$app['view']->assign('admin_memo', $admin_memo);
}

また、ここでは説明しませんが、コントローラーにも、テンプレートフックのフック名のように、コントローラー内部にフックポイントを持っており、そこにひっかけて処理させる方法もあります。
都合のよい場所にこのPHPフックがある場合は積極的に使っていきましょう。詳しくはこちら

ほしいところにテンプレートhookがないとき


先程はテンプレートのとても都合の良いところにhookが用意されていました。
でも、たまに欲しいところにちょうどhookがないときもあります。

そのときは、.tplをまるごと上書きすることでカスタマイズしてしまう方法があります。
しかしこれは、多くの場合非常に広い範囲に手を加えることになり、運用時においてもアップデート時においても、フックを用いたカスタマイズに比べて保守性が低いです。奥の手として考えておきましょう。

アドオンスキーマー


ここまで、アドオン開発におけるテンプレートとコントローラーの話をしてきましたが、アドオンとしてパッケージするとき
アドオンの基本情報をまとめたファイルがないとインストールできません。

場所は固定で

admin_memo(アドオンのフォルダ)/app/addons/admin_memo(アドオンID)/addon.xml

です。
ここに一例を示しますが、細かい設定項目についてはこちらを参照してください。
先程からディレクトリ構成のなかで度々登場しているアドオンIDもここで設定します。

<?xml version="1.0"?>
<addon scheme="3.0">
    <id>admin_memo</id>
    <version>1.0</version>
    <priority>700</priority>
    <position>0</position>
    <status>active</status>
    <default_language>ja</default_language>
    <auto_install>MULTIVENDOR,ULTIMATE</auto_install>
    <authors>
        <author>
            <name>株式会社あんどぷらす(Andplus Co. Ltd.)</name>
            <email>info@andplus.co.jp</email>
            <url>https://www.andplus.co.jp</url>
        </author>
    </authors>
    <supplier>株式会社あんどぷらす(Andplus Co. Ltd.)</supplier>
    <supplier_link>https://www.andplus.co.jp</supplier_link>
    <compatibility>
        <core_version>
            <min>4.13.2</min>
        </core_version>
        <core_edition>ULTIMATE</core_edition>
        <php_version>
            <min>7.0.0</min>
            <min>7.4.0</min>
        </php_version>
    </compatibility>
    <functions>
        <item for="install">fn_admin_memo_install</item>
        <item for="uninstall">fn_admin_memo_uninstall</item>
    </functions>
</addon>

最下部のインストール関数、アンインストール関数はこの後も登場します。よく使うので覚えておきましょう。

言語変数


ところで先程のテンプレートの中に{__(“original_admin_memo”)}という記述がありました。

<div class="control-group">
    <label for="remarks" class="control-label">{__("original_admin_memo")}</label>
    <div class="controls">
        <textarea class="input-large user-success" id="admin_memo" name="admin_memo" cols="32" rows="3"
            aria-invalid="false">{$admin_memo}</textarea>
    </div>
</div>

ここは、本来「管理者メモ」というフィールドのタイトルが入る部分ですが、{__(“ ”)}を用いた記述に変わっています。これを言語変数といいます。
もし、この部分を「管理者メモ」とベタ書きすると、CS-Cartの言語設定がなんであれ日本語で表示されてしまいます。
これを避けるために、設定された言語に応じて、文言を表示させる仕組みが言語変数です。俗にいう翻訳ファイルのようなものです。
言語変数は管理画面からも登録、編集できますが、アドオンインストール時にまとめてインストールできてしまうのでそれも設定してしまいましょう。

日本語の言語変数のファイルは

admin_memo(アドオンのフォルダ)/var/langs/ja/addons/admin_memo(アドオンID).po

英語の言語変数のファイルは

admin_memo(アドオンのフォルダ)/var/langs/en/addons/admin_memo(アドオンID).po

といった作り方になります。

今回はこの2つですが、多言語対応が必要な場合はその都度増やしましょう。

言語変数ファイルでは、アドオンの名前と説明の設定も行います。
例は以下になります。くわしい設定方法はこちら

msgid ""
msgstr "Project-Id-Version: tygh"
"Content-Type: text/plain; charset=UTF-8\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
 
 
msgctxt "Addons::name::admin_memo"
msgid "Administrator memo"
msgstr "管理者メモ"
 
msgctxt "Addons::description::admin_memo"
msgid "Add an administrator memo field to the user page of the administration page. Generated by <a target="_blank" href="https://www.andplus.co.jp">株式会社あんどぷらす(Andplus Co. Ltd.)</a>."
msgstr "管理画面のユーザーページに管理者メモフィールドを追加します。 Generated by <a target="_blank" href="https://www.andplus.co.jp">株式会社あんどぷらす(Andplus Co. Ltd.)</a>."
 
msgctxt "Languages::original_admin_memo"
msgid "Administrator memo"
msgstr "管理者メモ"

また、”original_admin_memo”は”admin_memo”でもいい気もしますが、先に”admin_memo”という言語変数があると上書いてしまったり、似た名前があると混乱のもとになるので、なるべくユニークな名前をつけましょう。
ルールを統一するために何かしらの接頭辞をつけることがおすすめです。

func.php


アドオン開発もいよいよ仕上げです。

func.phpは

app/addons/admin_memo(アドオンID)/func.php

に置いて、このアドオン内で用いたい関数を定義する役割があります。

また先程アドオンスキーマーで設定したインストール関数とアンインストール関数を定義するのもここです。
今回管理者メモフィールドに入力された値は”user_profiles”テーブルに格納しますが、”admin_memo”カラムがないのでインストール関数で追加します。
また、アンインストール時にカラムを落とす記述も一緒に書いておきましょう。

<?php
 
if (!defined('BOOTSTRAP')) { die('Access denied'); }
 
function fn_XXXX () {
     /*独自関数もここで定義します*/
}
 
function fn_admin_memo_install()
{
    db_query('ALTER TABLE `?:user_profiles` ADD `admin_memo` text NOT NULL DEFAULT ""');
}
 
function fn_admin_memo_uninstall()
{
    db_query('ALTER TABLE `?:user_profiles` DROP `admin_memo`');
}

おさらい

【第1層〜8層:admin_memoアドオン】
app
   ┣ addons
      ┣ admin_memo(アドオンID)
         ┣ addon.xml
         ┣ func.php
         ┣ controllers
            ┣ backend
               ┣ profiles.pre.php
design
   ┣ backend
      ┣ templates
         ┣ addons
            ┣ admin_memo(アドオンID)
               ┣ hooks
                  ┣ profiles
                     ┣ general_content.post.tpl
var
   ┣ langs
      ┣ en
      ┃  ┣ addons
      ┃  ┣ admin_memo.po
      ┣ ja
          ┣ addons
          ┣ admin_memo.po

これが完成したアドオンのディレクトリ構造です。
今回説明のために、なるべく最小構成になるようにしてみました。
カスタマイズ箇所が増えるともちろん枝分かれが増えていきますが、基本はここで説明したルールに従って細分化されるので一度分かり始めると、するするいけると思います。

今回、アドオン開発初心者にターゲットを絞って説明してきました。まず、何をどう手を付ければいいんだ…という人の助けになれば幸いです。
といっても、僕もまだまだ駆け出し小僧ですので、これ違うよ。って言うところあったらコメントにいただけると嬉しいです。

それでは!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

< すべての記事を見る >

Web制作の株式会社あんどぷらす
ECサイト構築サービス「ウルトコ」
イベントスペース「fuigo」
CS-Cartの情報ポータル「STOCK」