Skip to content

【システム】テーマから管理画面テンプレートを上書きできない問題 #4506

Description

@katokaisya

テーマから管理画面テンプレートを上書きできない問題

ステータス: 未解決

customAdminTheme を使ったコア修正のアプローチは、これまでに2パターン試したが
両方とも実機で管理画面が壊れることを確認した(詳細は「原因2」参照)。
現時点で安全に上書きする方法は見つかっていない。本番相当の環境にこのアプローチを
適用しないこと。

現象

plugins/{テーマ名}config.phptype => 'Theme')配下に
templates/Admin/element/... のようなファイルを配置し、baserCMS公式ドキュメント
https://baserproject.github.io/5/theme/dashboard)の案内通りに管理画面テンプレートを
上書きしようとしても、常にコア本体(例: vendor/baserproject/bc-admin-third/templates/Admin/...
側のファイルが使われてしまい、テーマ側のファイルが一切反映されない。

原因1: customAdminTheme がテーマに対して機能しない(コアのバグ)

  1. 管理画面の描画に使われる BaserCore\View\BcAdminAppView は、コア管理テーマ
    bc-admin-third をCakePHPネイティブの「テーマ」($this->theme)として保持している。
  2. CakePHPの View::_paths() は、テーマのパス($themePaths)を常にプラグイン自身の
    パスより優先
    して検索する。そのため element('{テーマ名}.Xxx/yyy') のように
    プラグイン指定で要素を呼んでも、bc-admin-third 自身が持つ同名ファイルが先に見つかり、
    {テーマ名} 側のファイルには到達しない。
  3. baserCMSは「他プラグインが管理画面テンプレートを上書きする」ケースのために
    BcApp.customAdminTheme という設定を用意しており、これが正しく機能していれば
    bc-admin-third のテーマパスより前にプラグインのパスを割り込ませることができる
    array_unshift で先頭に追加されるため)。
  4. しかし、その判定処理(vendor/baserproject/baser-core/src/View/BcAdminAppView.php
    _paths())は次のようになっている。
    $customAdminTheme = Configure::read('BcApp.customAdminTheme');
    $plugins = Hash::extract(BcUtil::getEnablePlugins(), '{n}.name');
    if (!$customAdminTheme || !in_array($customAdminTheme, $plugins)) {
        return $paths; // ここで早期リターンし、上書きパスが追加されない
    }
    BcUtil::getEnablePlugins() は、DBの plugins テーブルに status=true で登録された
    行のみを返す。
  5. 一方、テーマconfig.phptype => 'Theme')は BcPlugin::applyAsTheme()
    sites.theme カラムを更新するのみ)で適用され、plugins テーブルには一切登録されない
    BaserCorePlugin::addTheme()getEnablePlugins() を経由しない別経路でロードするため)。
  6. 結果として、{テーマ名} のようなテーマでは手順4の判定が常に失敗し、
    customAdminTheme を設定していても上書き用のパスが優先順位に追加されない。

これは実装上の考慮漏れによるbaserCMSコアのバグである。

原因2: フロント/管理画面兼用テーマでは、修正しただけでは危険(実機検証で判明)

原因1を修正して customAdminTheme を有効にしただけでは、そのテーマがフロント側の
テーマも兼ねている場合、管理画面全体のレイアウトが崩れる。
実際に検証して確認した。

仕組み

CakePHPは管理画面向けにテンプレート/レイアウト/要素を探す際、「Admin/付きパス」が
見つからなければ「Admin/無しパス」に同じルートディレクトリ内でフォールバックする
(プレフィックスのカスケード機能。Cake\View\View::_getSubPaths() の挙動)。

customAdminTheme の実装(後述の対処方法1)は、テーマのルート
plugins/{テーマ名}/templates/)をそのまま検索パスの先頭に追加する。すると、
このカスケードが同じテーマのルート内で働いてしまい、Admin/layout/default.php
無ければ、フロント用の layout/default.php に、Admin/Error/error500.php
無ければフロント用の Error/error500.php に、というように誤ってフォールバックする。

フロントテーマは通常 layout/default.phpError/*.phpelement/*.php を大量に
持っているため、{テーマ名} がフロントテーマを兼ねている場合はほぼ確実にこの衝突が起き、
管理画面のレイアウトが(フロント用テンプレートに差し替わることで)崩れる。

対処方法(修正版・実機検証の結果、これも不十分と判明)

追加するパスを、テーマのルートtemplates/)ではなく、あらかじめAdmin/まで
含めたパス(templates/Admin/)にすれば、カスケードのフォールバック先が常に
Admin/ 配下に留まり、フロント用テンプレートへの誤フォールバックは防げるはずだった。

// 対処方法1の array_unshift 部分(修正版)
array_unshift($paths, ROOT . DS . 'plugins' . DS . $theme . DS . 'templates' . DS . 'Admin' . DS);

しかし、実機で検証したところ、この修正版でも管理画面全体が崩れる現象が再現した。
テーマの templates/Admin/ 配下には上書き対象の1ファイル(例:
element/Dashboard/baser_news.php)しか存在しないにもかかわらず、管理画面のうち
テーマ側にテンプレートが無い画面まで含めて全体的に表示が崩れてしまう。

Admin/配下にスコープすればフロントへの誤フォールバックだけを防げる」という理論上の
分析は、実際の挙動と一致しなかった。_paths()単体を読んだだけでは説明できない、
他の仕組み(例: テンプレートパスのキャッシュ、coreAdminThemeとの相互作用、
アセット/CSS・JS解決への影響など)が絡んでいる可能性があるが、原因はまだ特定できて
いない
。オリジナル案・Admin/スコープ版のいずれも、実機検証で管理画面が壊れることを
確認しており、customAdminTheme を用いたコア修正のアプローチ自体を一旦保留し、
原因不明のまま本番相当の環境に適用しないこと。

対処方法(未解決・下記は実機検証で管理画面が壊れることを確認済み)

以下の3点は理論上すべて揃って初めて機能するはずだったが、実機検証の結果、
これを適用すると管理画面が壊れることを確認している(「原因2」参照)。現状は
参考情報として記録するのみで、このまま適用しないこと。

1. vendor/baserproject/baser-core/src/View/BcAdminAppView.php を修正する

_paths() の判定処理を、baserCMS独自の getEnablePlugins()plugins テーブル基準、
テーマは対象外)から、CakePHP標準の Cake\Core\Plugin::isLoaded()
(実際にロードされているプラグイン/テーマかどうかの判定。テーマも対象に含まれる)に
変更する。あわせて、追加するパスを Admin/ まで含めたものにする(原因2の対処)。

// Before
$paths = parent::_paths($plugin, $cached);
$customAdminTheme = Configure::read('BcApp.customAdminTheme');
$plugins = Hash::extract(BcUtil::getEnablePlugins(), '{n}.name');
if (!$customAdminTheme || !in_array($customAdminTheme, $plugins)) {
    return $paths;
}
$themes = [$customAdminTheme, Inflector::dasherize($customAdminTheme)];
foreach($themes as $theme) {
    array_unshift($paths,
        ROOT . DS
        . 'plugins' . DS
        . $theme . DS
        . 'templates' . DS
    );
}
return $paths;

// After
$paths = parent::_paths($plugin, $cached);
$customAdminTheme = Configure::read('BcApp.customAdminTheme');
if (!$customAdminTheme || !\Cake\Core\Plugin::isLoaded($customAdminTheme)) {
    return $paths;
}
$themes = [$customAdminTheme, Inflector::dasherize($customAdminTheme)];
foreach($themes as $theme) {
    array_unshift($paths,
        ROOT . DS
        . 'plugins' . DS
        . $theme . DS
        . 'templates' . DS
        . 'Admin' . DS
    );
}
return $paths;

_paths() 本来の意図(「customAdminTheme に指定された値が実在する読み込み済みの
プラグイン/テーマ名であること」の確認)に対して Plugin::isLoaded() の方が忠実であり、
通常プラグイン(type => 'Plugin')での既存の利用ケースには影響しない。

注意: これは vendor ファイルの直接編集(コアハック)である。composer update
baserproject/baser-core を更新すると、この修正は上書きされて消えるため、更新の都度
再適用が必要になる。

2. 対象テーマの config/setting.php に設定を追加する

plugins/{テーマ名}/config/setting.phpBcApp 配列に1行追加する。

'BcApp' => [
    // ...既存の設定...
    'customAdminTheme' => '{テーマ名}',
],

3. 上書き先ファイルの配置場所

plugins/{テーマ名}/templates/Admin/... 配下に、上書きしたいコア側のテンプレートと
同じ相対パスでファイルを配置する(例: templates/Admin/element/Dashboard/baser_news.php)。
ディレクトリ名の大文字小文字(Admin は先頭大文字)を、コア側と正確に一致させること
(Linux等の大文字小文字を区別するファイルシステムでは、これがずれていると上書きされない)。

結論

現時点では、こちらの機能はbaserCMSのコアでは難しく、パネルを除外するようなループを記述しないと実現が難しいです。
テーマ内のsrc/Plugin.phpなどに直接ループを記述することでも対応は可能ですが、プラグインなどで外部に出してしまった方が、再利用が楽だと思われます。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions