baserCMS 5系 テーマ開発パターン集
本スキルの対象は baserCMS 5系(CakePHP 5ベース)におけるテーマの新規開発・改修。5系のテーマはプラグイン形式であり、plugins/<ThemeName>/ に配置してテンプレート(templates/)・アセット(webroot/)・少数のフロント表示用 Helper(src/View/Helper/)で構成する。環境・ディレクトリ・命名・コアハック禁止などの共通ルールは basercms5-development、テーマに Controller/Table などプラグイン相当の内部コードを持たせる場合は basercms5-plugin-development、4系テーマからの移行作業は basercms-theme-4-to-5-upgrade を参照する。
1. テーマプラグインの構造
テーマは以下のディレクトリ構成で作る(コアの参考実装: vendor/baserproject/bc-front/templates/、サンプル: plugins/BcThemeSample/)。
plugins/<ThemeName>/
├── config.php … テーマ情報(return 配列。'type' => 'Theme' 必須)
├── src/<ThemeName>Plugin.php … プラグインクラス(bake 生成の雛形でよい)
├── src/View/Helper/ … テーマ独自のフロント表示用 Helper(任意)
├── templates/
│ ├── layout/ … レイアウト(default.php 等)
│ ├── element/ … 共通エレメント(header.php / footer.php 等)
│ ├── element/widget/ … ウィジェット用 element(§5)
│ ├── Pages/ … 固定ページテンプレート(default.php 等)
│ ├── Blog/<content>/ … ブログテンプレート(posts.php / index.php / single.php / archives.php)
│ └── Mail/<content>/ … メールフォームテンプレート(index.php / confirm.php / submit.php)
└── webroot/ … css / js / img(テーマアセット)
config.phpはreturn ['type' => 'Theme', 'title' => ..., 'description' => ..., 'author' => ..., 'url' => ...];の形式で書く。- レイアウト
templates/layout/default.phpでは本文を$this->fetch('content')で出力し、<head>内は$this->BcBaser->charset()/title()/metaDescription()/css()/js()/scripts()、</body>直前で$this->BcBaser->func()等の BcBaser API を使って組み立てる。 - 固定ページテンプレート
templates/Pages/default.phpは、管理画面で編集された本文をecho $page->contentsで出力する($pageは Page エンティティ)。 - コンテンツ別テンプレは
templates/Blog/<コンテンツのテンプレート名>/posts.phpのように「機能プラグイン名/テンプレート名/アクション名」で配置し、管理画面のコンテンツ設定で選択されたテンプレート名のディレクトリが使われる。テーマに無いテンプレートはコア(bc-front/templates/plugin/BcBlog/plugin/BcMail/等)へフォールバックする。 webroot/配下のアセットは/テーマ名snake形式/css/style.cssのような URL(プラグイン webroot 配信)で配信される(§4)。- テーマの適用は管理画面(テーマ管理)から行う。開発環境で直接切り替える場合は
sitesテーブルのthemeカラムを UPDATE してキャッシュクリア(bin/cake cache clear_all)する。
2. View・ビュー変数の作法
layout / element / テンプレート内の $this は View 自身である。Helper 内の $this->getView() と混同しない(View で $this->getView() を呼ぶと Call to undefined method ...View::getView())。
コントローラが供給するビュー変数に依存せず、テンプレート側で必要な値を自前で導出するのが基本。コアはフロントの全ビューへ $user 等のグローバル変数を供給しない。
// ログインユーザー(UserInterface|false — false があり得るので必ず判定する)
$user = \BaserCore\Utility\BcUtil::loginUser();
$authPrefixes = $user ? $user->getAuthPrefixes() : [];
// 現在のプレフィックス(Front / Admin など)
$currentPrefix = \BaserCore\Utility\BcUtil::getRequestPrefix($this->getRequest());
// セッション
$session = $this->getRequest()->getSession();
現在のコンテンツ・サイト情報はリクエスト属性から取る。どちらもエンティティで、コンテキストによっては null があり得るため ?-> で参照する。
$currentContent = $this->getRequest()->getAttribute('currentContent'); // Content エンティティ|null
$currentSite = $this->getRequest()->getAttribute('currentSite'); // Site エンティティ|null
$entityId = $currentContent?->entity_id; // 機能側レコードID(ブログコンテンツID 等)
$siteId = $currentSite?->id;
現在のURLパスは $this->getRequest()->getPath() と書く(先頭スラッシュ付きで返る)。
3. ヘルパ
BcBaser の委譲機構(PluginBaserHelper)
$this->BcBaser->blogPosts('news', 5) のような機能系ショートカットは、BcBaserHelper 自体のメソッドではなく、各プラグインの Bc<Plugin>BaserHelper::methods() が返すマッピングテーブル経由で __call に委譲される(例: bc-blog の BcBlogBaserHelper::methods() に 'blogPosts' => ['Blog', 'posts'])。
- 実在確認は2段階で行う: ①
grep -rln "function <名前>" vendor/baserproject/で関数定義を探す → ②見つからなければgrep -rn "'<名前>'" vendor/baserproject/*/src/View/Helper/Bc*BaserHelper.phpでmethods()マッピングを探す。マッピング委譲のため関数定義が無いのが正常であり、①だけで「存在しない」と判定しない。 - 委譲先メソッドは提供プラグインが有効なときだけ動く。無効時は BadMethodCall になる。
よく使う静的ユーティリティ
\BaserCore\Utility\BcSiteConfig::get('formal_name'); // サイト基本設定(address / name 等のキー)
\BaserCore\Utility\BcUtil::fullUrl($url); // フルURL生成
ヘルパの動的ロード
ヘルパは new でインスタンス化せず(DIコンテナ未注入で getService() が失敗する)、View からロードする。loadHelper() はインスタンスを返す。
$blog = $this->getView()->loadHelper('BcBlog.Blog'); // Helper 内から
$blog = $this->loadHelper('BcBlog.Blog'); // テンプレート内($this が View)から
参照名の規約と他プラグイン依存のガード
- コア機能の参照名は
Bc接頭辞で書く: ヘルパBcBlog.Blog、テーブルTableRegistry::getTableLocator()->get('BcBlog.BlogPosts')、element$this->BcBaser->element('BcBlog.xxx')。コアテーブルは複数形+プラグイン接頭辞(BaserCore.Contents/BaserCore.Sites)。 - テーマから他プラグインのヘルパを呼ぶ場合は、プラグイン無効時にテーマごと落ちないよう
Plugin::isLoaded()でガードし、必要なら loadHelper する。
if (\Cake\Core\Plugin::isLoaded('<Plugin>')) {
$helper = $this->helpers()->has('Sample')
? $this->Sample
: $this->loadHelper('<Plugin>.Sample');
$helper->show(['num' => 3]);
}
4. アセットとURL
css() / js() のシグネチャ
BcBaser->css($path, $inline = true, $options = [])(js() も同様)。第2引数は $inline(false でバッファに溜めて scripts() で出力)で、media 等の属性は第3引数に書く。
$this->BcBaser->css('style'); // <link ...>
$this->BcBaser->css(['print'], true, ['media' => 'print']); // 属性は第3引数
$this->BcBaser->js('theme', true, ['defer' => true]);
URL はハードコードしない
リンク・画像などの URL はルート相対(/about/ 等)をハードコードせず BcBaser->getUrl('/path') を通して書く。サブディレクトリ設置時に base が自動付与される。
<a href="<?php echo $this->BcBaser->getUrl('/about/') ?>">会社概要</a>
<?php $this->BcBaser->link('会社概要', '/about/') ?>
Helper 内で URL 文字列を組み立てる場合は \Cake\Routing\Router::url($url) を通すか、リクエストの webroot を前置する: $this->getView()->getRequest()->getAttribute('webroot') . ltrim($url, '/')。
アップロード画像の表示(BcUploadHelper::uploadImage)
uploadImage('<field>', $entity, $options) と書く——第1引数は素のフィールド名、第2引数は EntityInterface 必須(文字列不可)。BcUploadHelper はデフォルトで View の plugin/name からテーブルを解決するため、テーマや他プラグインのテンプレから使うときは 'table' => '<Plugin>.<Tables>'(BcUpload behavior 持ち)を options で明示する。画像サイズは 'imgsize'、無い時の代替は 'noimage'。file 型を含むメールフォーム系テンプレは §6 の setTableToUpload() を参照。
テーマアセットの配信形式
テーマの webroot/ 配下はテーマ名の snake_case をプレフィックスとした URL で配信される。
$this->BcBaser->css('style'); // <ThemeName> の webroot/css/style.css を解決
// URL を直接組む場合
$this->BcBaser->getUrl('/sample_theme/css/style.css'); // plugins/SampleTheme/webroot/css/style.css
5. ウィジェット
ウィジェットエリアに表示する element は templates/element/widget/(単数形 widget。widgets ではない)に置く。BcWidgetArea はこのパスを探索する(コアの配置例: bc-front/templates/plugin/BcBlog/element/widget/blog_recent_entries.php)。
- プラグインが提供するウィジェット(例:
BcBlog.widget/blog_recent_entries)をテーマでオーバーライドする場合も、テーマ側の同パスtemplates/element/widget/<名前>.phpに置く。 - 管理画面向けのウィジェット設定用 element は
templates/Admin/element/widget/に置く。
6. メールフォームテンプレート
メールフォームテンプレートは templates/Mail/<テンプレート名>/index.php(入力)・confirm.php(確認)・submit.php(完了)で構成し、フォーム描画は BcBaser の mailForm 系 API で書く(BcMailBaserHelper::methods() が Mailform ヘルパへ橋渡しする)。コアの正本は vendor/baserproject/bc-front/templates/plugin/BcMail/ にあり、迷ったらこれに合わせる。
フォーム生成は、コントローラがセットするエンティティ $mailMessage を第1引数に取り、valueSources を指定する。
$this->BcBaser->createMailForm($mailMessage, array_merge($options, ['valueSources' => ['context']]));
主要 API(フィールド名は素の名前で書く。MailMessage. 接頭辞の文脈は内部処理される):
| 用途 | 書き方 |
|---|---|
| hidden フィールド | $this->BcBaser->mailFormHidden('mode', ['id' => 'MailMessageMode']) |
| フィールドのアンロック | $this->BcBaser->unlockMailFormField('mode') |
| 送信ボタン | $this->BcBaser->mailFormSubmit($caption, $options) |
| エラー表示 | $this->BcBaser->mailFormError('field_name', $message) |
| 画像認証 | $this->BcBaser->mailFormAuthCaptcha('auth_captcha', ['helper' => $this->BcBaser]) |
| フォーム終了 | $this->BcBaser->endMailForm() |
| フリーズ(confirm.php) | $this->BcBaser->freezeMailForm() |
| フィールド描画 | $this->BcBaser->mailFormControl($fieldName, $options) |
| ラベル | $this->BcBaser->mailFormLabel($fieldName, $text) |
| 入力値の取得 | $this->BcBaser->getMailFormSourceValue($fieldName) |
| グループ最終フィールド判定 | $this->BcBaser->isMailFormGroupLastField($mailFields, $field) |
mailFormHidden('mode')は素のフィールド名だと id がmodeになる。JS が#MailMessageModeを参照する場合は['id' => 'MailMessageMode']を明示する。- file 型フィールドを描画するテンプレート(index.php / confirm.php)の先頭には必ず次の1行を書く。無いと
BcUploadHelper を利用するには … table … を指定してください(BcException)で落ちる。
$this->BcBaser->setTableToUpload('BcMail.MailMessages');
- フィールド一覧を描画する
element('mail_input')はコア版(bc-front/.../BcMail/element/mail_input.php)にフォールバックするため、独自デザインが不要ならテーマに置かなくてよい。
7. ブログテンプレート
ブログテンプレートは templates/Blog/<テンプレート名>/ に index.php(一覧)・archives.php(カテゴリ/タグ/年月アーカイブ)・single.php(詳細)・posts.php(BcBaser->blogPosts() から呼ばれるパーツ用一覧)を置く。
$posts/$postはエンティティ(またはその ResultSet)。関連もプロパティアクセスで書く:$post->title、$post->content、$post->detail、$post->blog_category->title、$post->blog_content_id。- 投稿日時のカラム名は
posted。ソート指定は'sort' => 'posted'/orderBy(['BlogPosts.posted' => 'DESC'])と書く。 - アイキャッチは
$this->Blog->getEyeCatch($post, ['width' => 300, 'link' => true])または BcBaser 委譲の$this->BcBaser->blogPostEyeCatch($post, [...])で描画する。存在判定は$post->eye_catchで行う。 - 記事リンク・日付・カテゴリ・タイトルも BcBaser 委譲 API で書ける:
getBlogPostLinkUrl($post)/blogPostDate($post, 'Y.m.d')/blogPostCategory($post)/blogPostTitle($post, false)/blogPostContent($post, true, false, 46)。 - カテゴリ一覧・タグ一覧は Blog ヘルパ/element で描画する(コアの
element/blog_category_list.php/blog_tag_list.phpを参考にする)。 - Table を直接引いて記事を取得する場合は
TableRegistry::getTableLocator()->get('BcBlog.BlogPosts')からクエリビルダで書き、公開条件はgetConditionAllowPublish()を使う。
$blogPostsTable = \Cake\ORM\TableRegistry::getTableLocator()->get('BcBlog.BlogPosts');
$posts = $blogPostsTable->find()
->where($blogPostsTable->getConditionAllowPublish())
->where(['BlogPosts.blog_content_id' => $blogContentId])
->orderBy(['BlogPosts.posted' => 'DESC'])
->limit($num)
->all();
同種の取得ロジックが複数テンプレートに散在する場合は、テーマの src/View/Helper/<ThemeName>Helper.php に集約する。