- functions.phpを分割する狙いと全体像
- 推奨ディレクトリ構成
- functions.phpは「読むだけ」にする
- 00-setup.php(テーマの初期化)
- 10-enqueue.php(CSS/JSの読み込み)
- 40-cpt.php(カスタム投稿タイプの登録)
- 20-admin.php(管理画面の調整)
- 30-security.php(小さなセキュリティ対策)
- 50-shortcodes.php(ショートコードの例)
- 90-helpers.php(ヘルパー関数の置き場)
- 読み込み順と命名規則のコツ
- 子テーマ対応のベストプラクティス
- プラグイン領域とテーマ領域の線引き
- 移行手順とデバッグ
- よくある落とし穴と対策
- まとめ
functions.phpを分割する狙いと全体像
テーマの成長に合わせてfunctions.phpが肥大化すると可読性や保守性が急速に落ちます。役割ごとにファイルを分割し、functions.phpには「読み込み管理」だけを残すことで、どこを直せばよいかが一目で分かり、レビューや差分確認も容易になります。基本方針は役割の境界を明確にして命名規則を統一し、読み込み順を制御しつつ子テーマにも安全に対応させることです。
推奨ディレクトリ構成
your-theme/
├─ functions.php … 読み込み管理(司令塔)
├─ inc/ … 機能モジュール置き場
│ ├─ 00-setup.php … テーマセットアップ(title-tag, サムネイル等)
│ ├─ 10-enqueue.php … CSS/JSの読み込み
│ ├─ 20-admin.php … 管理画面カスタマイズ
│ ├─ 30-security.php … セキュリティ微調整
│ ├─ 40-cpt.php … カスタム投稿タイプ
│ ├─ 50-shortcodes.php … ショートコード
│ └─ 90-helpers.php … ヘルパー関数群
└─ assets/ … CSS/JS/画像 など
functions.phpは「読むだけ」にする
<?php
// 子テーマ優先で安全にパスを解決しつつ、読み込み順を固定
$includes = [
'inc/00-setup.php',
'inc/10-enqueue.php',
'inc/20-admin.php',
'inc/30-security.php',
'inc/40-cpt.php',
'inc/50-shortcodes.php',
'inc/90-helpers.php',
];
foreach ($includes as $rel_path) {
$file = get_theme_file_path($rel_path); // 子テーマがあれば子を優先
if (file_exists($file)) {
require_once $file;
}
}
// すべてのファイルを自動で取り込みたい場合(順序はファイル名の昇順)
// foreach (glob(get_theme_file_path('inc/*.php')) as $file) { require_once $file; }
00-setup.php(テーマの初期化)
<?php
// すべての関数に一貫した接頭辞を付けて衝突を回避
function cls_theme_setup() {
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', ['search-form','gallery','caption','script','style']);
register_nav_menus([
'global' => 'グローバルナビ',
'footer' => 'フッターナビ',
]);
add_image_size('thumb-wide', 1200, 675, true);
}
add_action('after_setup_theme', 'cls_theme_setup');
10-enqueue.php(CSS/JSの読み込み)
<?php
function cls_enqueue_assets() {
$uri = get_theme_file_uri();
$path = get_theme_file_path();
$ver = function($rel) use ($path) {
$file = $path . '/' . ltrim($rel, '/');
return file_exists($file) ? filemtime($file) : null;
};
wp_enqueue_style('cls-common', $uri . '/assets/css/common.css', [], $ver('assets/css/common.css'));
wp_enqueue_script('jquery'); // WP同梱
wp_enqueue_script(
'cls-common-js',
$uri . '/assets/js/common.js',
['jquery'],
$ver('assets/js/common.js'),
true
);
if (is_front_page()) {
wp_enqueue_style('cls-home', $uri . '/assets/css/home.css', ['cls-common'], $ver('assets/css/home.css'));
}
}
add_action('wp_enqueue_scripts', 'cls_enqueue_assets');
40-cpt.php(カスタム投稿タイプの登録)
<?php
function cls_register_cpt_news() {
$labels = [
'name' => 'ニュース',
'singular_name' => 'ニュース',
'add_new_item' => 'ニュースを追加',
];
$args = [
'label' => 'ニュース',
'labels' => $labels,
'public' => true,
'has_archive' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-megaphone',
'supports' => ['title','editor','thumbnail','excerpt','custom-fields'],
'show_in_rest' => true,
'rewrite' => ['slug' => 'news'],
];
register_post_type('news', $args);
}
add_action('init', 'cls_register_cpt_news');
20-admin.php(管理画面の調整)
<?php
function cls_admin_assets($hook) {
if ($hook === 'post.php' || $hook === 'post-new.php') {
$rel = 'assets/css/admin.css';
wp_enqueue_style('cls-admin', get_theme_file_uri($rel), [], filemtime(get_theme_file_path($rel)));
}
}
add_action('admin_enqueue_scripts', 'cls_admin_assets');
function cls_admin_columns_news($columns) {
$columns['thumbnail'] = 'サムネイル';
return $columns;
}
add_filter('manage_news_posts_columns', 'cls_admin_columns_news');
function cls_admin_column_news_content($column, $post_id) {
if ($column === 'thumbnail') {
echo get_the_post_thumbnail($post_id, [60,60]);
}
}
add_action('manage_news_posts_custom_column', 'cls_admin_column_news_content', 10, 2);
30-security.php(小さなセキュリティ対策)
<?php
remove_action('wp_head', 'wp_generator'); // WPバージョン出力を抑止
add_filter('xmlrpc_enabled', '__return_false'); // XML-RPC無効化(必要時は解除)
function cls_login_errors_generic() {
return 'ログイン情報が正しくありません。';
}
add_filter('login_errors', 'cls_login_errors_generic');
50-shortcodes.php(ショートコードの例)
<?php
function cls_sc_btn($atts, $content = null) {
$a = shortcode_atts([
'href' => '#',
'target' => '_self',
], $atts, 'btn');
$label = $content ?: '詳細を見る';
return '<a class="c-btn" href="' . esc_url($a['href']) . '" target="' . esc_attr($a['target']) . '" rel="noopener">' . esc_html($label) . '</a>';
}
add_shortcode('btn', 'cls_sc_btn');
90-helpers.php(ヘルパー関数の置き場)
<?php
function cls_asset_ver($rel_path) {
$file = get_theme_file_path($rel_path);
return file_exists($file) ? filemtime($file) : null;
}
読み込み順と命名規則のコツ
読み込み順は依存関係が少ない基盤から積み上げるのが安定します。テーマの初期化が最初、スクリプト読み込み、管理画面調整、セキュリティ、機能追加(CPTやショートコード)、最後にヘルパーの順に並べると把握しやすく、番号プレフィックスで昇順読み込みにしておくと自動取り込みにも適合します。関数名の衝突を避けるため接頭辞を必ず付け、ファイル名は英数字とハイフンのみで統一し、役割が一見して分かる命名にします。
子テーマ対応のベストプラクティス
get_template_directory()とget_stylesheet_directory()の混用はバグの温床です。子テーマ優先解決が必要な場面ではget_theme_file_path()とget_theme_file_uri()を使うと、子テーマが存在する場合は子側、無い場合は親側のファイルを自動で参照してくれるため安全です。インクルード先でも同じ関数でパスやURIを解決し、親子の差し替えを妨げない実装に統一します。
プラグイン領域とテーマ領域の線引き
フロントの見た目に依存しないロジック(CPTやショートコード、管理画面の業務ロジック等)は本来プラグイン領域に置くのが理想です。まずはテーマ内で分割して運用し、再利用性が見えてきた段階でmu-pluginsや独自プラグインへ段階的に移すと移行がスムーズです。
移行手順とデバッグ
既存のfunctions.phpから対象コードを役割ごとに切り出し、同名関数の重複や未定義フック順の問題がないかをWP_DEBUG有効化とerror_logで確認します。切り出しはセットアップから着手し、その後enqueue、管理画面、CPT、ショートコードの順に分けると影響範囲を限定できます。読み込み漏れは致命的です。require_onceのパス解決にget_theme_file_path()を使い、file_exists()で存在チェックを行うと事故を防げます。
よくある落とし穴と対策
読み込み順の逆転による未定義関数参照、子テーマでのパス不一致、管理画面専用コードをフロントで動かしてエラーになる、キャッシュでCSS/JSが更新されない、といった事象が典型的です。順序は番号付きファイル名で縛り、パスはget_theme_file_*系で統一し、admin_enqueue_scriptsなど適切なフックを選択、アセットにはfilemtimeをバージョンに使ってキャッシュを常に更新する実装にしておくと安定します。
まとめ
functions.phpを分割する目的はファイルを増やすことではなく、責務を分けて変更範囲を最小化し、将来の拡張とチーム開発を容易にすることにあります。司令塔としてのfunctions.phpはインクルード以外の処理を置かず、役割ごとに整理されたinc配下へ集約、子テーマに安全なパス解決、番号付きの読み込み順、接頭辞での衝突回避、filemtimeによるキャッシュバスター、この一連の型を守ればテーマは読みやすく壊れにくくなり、長期運用でも安定したメンテナンス性を保てます。