エンジニアの端くれとして、たかがWordPressテーマであっても、綺麗なモジュール設計がしたいわけです。
そこで、以前作ったサイトのテーマをモチーフに、テーマファイルを再設計してみました。
対象にしたサイトは、ユーザーが会員登録し、PDFコンテンツを投稿でき、ダウンロード結果を集計できるなど、そこそこ高機能なサイトです。
綺麗なテーマファイル設計の条件
設計を行うにあたり、指針として設定した要件は以下の通りです。
1. オブジェクト志向であること
2. MVCライクであること
3. デプロイが容易であること
4. 他のテーマ開発への転用がしやすいこと
functions.php 晒し
なにはともあれ、まずはカスタマイズの要である functions.php の中身を公開します。
1 2 3 |
require_once( TEMPLATEPATH . '/includes/class-mytheme.php' ); $MT = new Mytheme(); $MT->init(); |
以上。
非常にシンプルですね。
ファイル構成
全体のファイル構成としては、以下のようになります。
/mytheme
– /includes
– – /hook
– – /module
– – /controller
– – /admin
– – class-mytheme.php
– – class-mt-const.php
– /libs
– /parts
– /admin
– /img
– /resource
– functions.php
– style.css
– index.php
– header.php
– footer.php
– sidebar.php
– ・・・
includesフォルダの中に、テーマをカスタマイズするためのクラス群が入ります。
class-mytheme.php に定義した Mytheme クラスが、テーマのコアクラスと呼んでいるものです。
クラス名はテーマ名として、その略称をその他のクラスのプレフィクスにしています。
hooks フォルダには、各種のアクションフック、フィルターフックを登録するためのメソッドをカテゴリごとに分けたクラスファイルが入っています。
module フォルダには、テンプレートやコントローラで使用するための汎用的なクラスファイルが入ります。
controller フォルダには、各ページで使用するコントローラクラスが、admin フォルダには、管理画面内に設定ページや機能ページを表示するためのクラスを配置します。
その他の第一階層のフォルダは、以下の通りです。
libs 外部ライブラリファイル
parts 複数のテンプレートで共有するパーツ
admin 管理画面内のページ用テンプレート
img 画像ファイル
resource CSSやJSファイル
テーマの「コアクラス」
そしてこのテーマ構成の肝である、Mytheme クラスの中身は、以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
<?php /** * テーマのコアクラス。 * functions.php で初期化される。 * * @category includes * @package mytheme * @author AD5 */ class Mytheme { const PACKAGE_PREFIX = 'MT_'; const INCLUDE_PATH = '/includes/'; const CLASS_FILE_PREFIX = 'class-'; const CONTROLLER_SUFFIX = '_Controller'; const ADMIN_PROCESSOR_PREFIX = 'Admin_Processor_'; const HOOK_CLASSES = array( 'Setting', 'Head', 'Query', 'Activate', 'Admin', 'Cron' ); const ADMIN_PROCS = array( 'setting', 'management' ); private $properties = array(); /** * 初期化 */ public function init() { //タイムゾーンセット date_default_timezone_set( 'Asia/Tokyo' ); //Built-Inプラグインのロード include_once( TEMPLATEPATH . '/lib/acf/acf.php' ); include_once( TEMPLATEPATH . '/lib/acf-repeater/acf-repeater.php' ); //クラスファイルのAutoLoad spl_autoload_register( array( $this, 'autoload' ) ); //各種フックの登録 $this->bind_hook( self::HOOK_CLASSES ); //管理画面の処理 $this->admin_process( self::ADMIN_PROCS ); //コントローラーのルーティング $this->route_controller(); } /** * オートロード(spl_autoload_registerのコールバック) */ public function autoload( $class ) { $directories = array( TEMPLATEPATH . self::INCLUDE_PATH, TEMPLATEPATH . self::INCLUDE_PATH . 'controller/', TEMPLATEPATH . self::INCLUDE_PATH . 'admin/', TEMPLATEPATH . self::INCLUDE_PATH . 'module/', TEMPLATEPATH . self::INCLUDE_PATH . 'hooks/', ); foreach ( $directories as $dir ) { $class_path = $dir . $this->get_classfile_name( $class ); if ( is_file( $class_path ) ) { require( $class_path ); return; } } } /** * クラス名からクラスファイル名を取得 */ private function get_classfile_name( $class ) { $classfile_name = self::CLASS_FILE_PREFIX . strtolower( str_replace( '_', '-', $class ) ) . '.php'; return $classfile_name; } /** * フック用クラスの初期化 */ public function bind_hook( $hooks ) { foreach ( $hooks as $hook ) { $class = self::PACKAGE_PREFIX . $hook; if ( class_exists( $class ) ) { $instance = new $class(); if ( method_exists( $instance, 'init' ) ) { $instance->init(); } } } } /** * 管理画面用処理クラスの展開 */ public function admin_process( $procs ) { foreach ( $procs as $proc ) { $class = self::PACKAGE_PREFIX . self::ADMIN_PROCESSOR_PREFIX . $proc; if ( class_exists( $class ) ) { $processor = new $class(); if ( method_exists( $processor, 'get_config' ) ) { $config = $processor->get_config(); if ( ! empty( $config['pages'] ) ) { $title = ! empty( $config['title'] ) ? $config['title'] : get_bloginfo( 'name' ); $type = ! empty( $config['type'] ) ? $config['type'] : 'manage_option'; $position = ! empty( $config['position'] ) ? $config['position'] : 20; $parent_slug = ""; foreach ( $config['pages'] as $slug => $page ) { $page_slug = self::kebab_prefix() . strtolower($proc) . '-' . $slug; $callback = array( $processor, $slug . '_view' ); //親メニューページの追加 if ( ! $parent_slug ) { add_action( 'admin_menu', function () use ( $title, $type, $page_slug, $callback, $position ) { add_menu_page( $title, $title, $type, $page_slug, $callback, '', $position ); } ); $parent_slug = $page_slug; } //子メニューページの追加 add_action( 'admin_menu', function () use ( $parent_slug, $type, $page_slug, $callback, $page ) { $suffix_menu = add_submenu_page( $parent_slug, $page['title'], $page['title'], $type, $page_slug, $callback, '' ); //scriptの追加 if ( ! empty( $page['script'] ) ) { add_action( "admin_print_scripts-" . $suffix_menu , function () use ( $page ) { foreach ( $page['script'] as $script ) { if ( ! empty( $script['path'] ) ) { wp_enqueue_script( $script['name'], get_template_directory_uri() . $script['path'] ); } else { wp_enqueue_script( $script['name'] ); } } } ); } //styleの追加 if ( ! empty( $page['style'] ) ) { add_action( "admin_head-" . $suffix_menu , function () use ( $page ) { foreach ( $page['style'] as $style ) { if ( ! empty( $style['path'] ) ) { wp_enqueue_script( $style['name'], get_template_directory_uri() . $style['path'] ); } else { wp_enqueue_script( $style['name'] ); } } } ); } } ); //admin_initで実行する処理の追加 if ( ! empty( $page['init_action'] ) ) { if ( ! empty( $_GET['page'] ) && $_GET['page'] == $page_slug ) { add_action( 'admin_init', array( $processor, $slug . '_action' ) ); } } //オプショングループの追加 if ( ! empty( $page['options'] ) ) { add_action( 'admin_init', function () use ( $proc, $slug, $page ) { foreach ( $page['options'] as $option ) { $group = self::kebab_prefix() . strtolower($proc) . '-' . $slug . '-option-group'; register_setting( $group, $option ); } } ); } } } } } } } public static function kebab_prefix() { return str_replace( '_', '-', strtolower( self::PACKAGE_PREFIX ) ); } /** * コントローラのフック */ public function route_controller() { add_action( 'template_include', array( $this, 'apply_controller' ), 9999 ); } public function apply_controller( $template ) { $class = self::PACKAGE_PREFIX . strtr( ucwords( strtr( basename( $template, '.php' ), array( '-' => ' ' ) ) ), array( ' ' => '_' ) ) . self::CONTROLLER_SUFFIX; if ( class_exists( $class ) ) { $controller = new $class(); global $post; if ( ! empty( $post->post_name ) && method_exists( $controller, $post->post_name . '_action' ) ) { $action = $post->post_name . '_action'; } else { $action = 'index_action'; } if ( method_exists( $controller, 'init' ) ) { $controller->init(); } $return = $controller->$action(); if ( $return ) { foreach ( $return as $key => $value ) { $this->properties[$key] = $value; } } } return $template; } public function get( $key ) { if ( array_key_exists( $key, $this->properties ) ) { return $this->properties[$key]; } else { return false; } } } |
このファイル自体は、他のテーマに流用する場合でも、const 値以外ほとんど変更することがありません。
いわば、フレームワークのコアに当たるものです。
以下、最初に定義した要件ごとに分けて解説していきます。