WordPressテーマのモジュール設計
-
綺麗なテーマファイル設計の条件
《参照サイト》- ●設計を行うにあたり、指針として設定した要件
-
- オブジェクト志向であること
- MVCライクであること
- デプロイが容易であること
- 他のテーマ開発への転用がしやすいこと
-
- ●functions.phpのソースコード
-
<?php require_once( STYLESHEETPATH . '/includes/class-mytheme.php' ); $MT = new Mytheme(); $MT->init(); //#EOF
-
- ●ファイル構成
-
/mytheme + /includes テーマをカスタマイズするためのクラス群 + /hook 各種のアクションフック、フィルターフックを登録するためのメソッドをカテゴリごとに分けたクラスファイルを配置する + /module テンプレートやコントローラで使用するための汎用的なクラスファイルを配置する + /controller 各ページで使用するコントローラクラスを配置する + /admin 管理画面内に設定ページや機能ページを表示するためのクラスを配置する – class-mytheme.php テーマのコアクラス – class-mt-const.php テーマ名の略称をその他のクラスのプレフィクスとする + /libs 外部ライブラリファイル + /parts 複数のテンプレートで共有するパーツ + /admin 管理画面内のページ用テンプレート + /img 画像ファイル + /resource CSSやJSファイル – functions.php – style.css – index.php – header.php – footer.php – sidebar.php – ・・・
-
- ●テーマの「コアクラス」
-
<?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; } } }
-
- ●Mytheme の init() メソッドでオブジェクト志向の恩恵を受けるため、オートローディングの設定を行なう
-
class Mytheme { //略 public function init() { //略 //クラスファイルのAutoLoad spl_autoload_register( array( $this, 'autoload' ) ); //略 } /** * オートロード(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; } //略 } includes 内で定義したクラスは、require_once しなくても使える。
-
- ●functions.php に羅列することの多いWordPressのフック登録処理は、hooks 配下のクラスファイルに、カテゴリごとに分けて記述する
-
/** * HEAD内の出力関連のフック用クラス * * @category includes / hooks * @package mytheme * @author AD5 */ class MT_Head { public function init() { add_action( 'init', array( $this, 'initialize' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'styles_and_scripts' ) ); add_filter( 'pre_get_document_title', array( $this, 'document_title' ) ); //略 } /** * ACTION HOOK : init * head内のゴミ削除 */ public function initialize() { remove_action( 'wp_head', 'wp_generator' ); remove_action( 'wp_head', 'rsd_link' ); remove_action( 'wp_head', 'wlwmanifest_link' ); remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); remove_action( 'wp_print_styles', 'print_emoji_styles' ); remove_action( 'admin_print_styles', 'print_emoji_styles' ); remove_filter( 'the_content_feed', 'wp_staticize_emoji' ); remove_filter( 'comment_text_rss', 'wp_staticize_emoji' ); remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); } /** * ACTION HOOK : wp_enqueue_scripts * CSS・JSの読み込み */ public function styles_and_scripts() { wp_enqueue_style( 'style', get_stylesheet_uri() ); wp_enqueue_style( 'font-awesome', get_template_directory_uri() . '/lib/css/font-awesome.css' ); wp_enqueue_script( 'jquery' ); } /** * FILTER HOOK : pre_get_document_title * タイトルタグの設定 */ public function document_title( $title ) { global $MT; return $MT->Meta->get_document_title(); } //略 }
-
《フックの種類ごとにクラス分け》
- ●MT_Setting クラス
- 投稿タイプやタクソノミー、リライトルールなどに関するフックを登録する
- ●MT_Query クラス
- pre_get_posts などクエリ関連のフックを登録する
- ●MT_Activate クラス
- テーマアクティベーション時の処理を登録する
- ●MT_Admin クラス
- 管理画面関連のフックを登録する
- ●MT_Ajax クラス
- AJAX関連のフックを登録する
- ●MT_Cron クラス
- Cron関連のフックを登録する
-
- ●実行するのは、コアクラスの、bind_hook() メソッドで init() メソッド内でコールされる
class Mytheme { //略 /** * 初期化 */ public function init() { //略 //各種フックの登録 $this->bind_hook( self::HOOK_CLASSES ); //略 } //略 /** * フック用クラスの初期化 */ 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(); } } } } //略 } hooks ディレクトリ内にあるクラスを順次インスタンス化し、init() メソッドを実行
-
《管理画面内にテーマ設定メニューとデータ管理メニューを追加し、複数のサブメニューを用意》
- ●管理画面内にページを作成する際には、メニューの追加やページの表示、オプションの登録など、共通する処理が多いので、メニュー単位で「プロセッサ」と呼んでいるクラスにまとめている
/** * 管理画面のサイト設定機能プロセス * * @category includes / admin * @package mytheme * @author AD5 */ class MT_Admin_Processor_Setting { public function get_config() { return array( 'title' => 'テーマ設定', 'type' => 'manage_options', 'position' => 21, 'pages' => array( 'main' => array( 'title' => '文言設定', 'options' => array( 'mt_home_main_message', 'mt_single_report_download_notice', ) ), 'mail' => array( 'title' => 'メール設定', 'options' => array( 'mt_mail_inquiry_notification_from', 'mt_mail_inquiry_notification_fromname', 'mt_mail_inquiry_notification_to', 'mt_mail_inquiry_notification_subject', 'mt_mail_inquiry_notification_content', ) ), ) ); } function main_view() { include( TEMPLATEPATH . '/admin/setting-main.php' ); } function mail_view() { include( TEMPLATEPATH . '/admin/setting-mail.php' ); } } get_config() メソッドが返す配列に従って、コアクラスの admin_process() メソッドが、メニューの追加処理、ページの表示処理、オプションの登録処理などを自動的に行っている
module フォルダ内のクラスは、複数のコントローラで共通して行う処理をまとめたユーティリティ的なクラスや、テンプレートで使用する静的メソッドを集めたクラスが入っている
-
《テーマ開発でMVCモデルを実現》
- ●ロジックと表示の分離実現
class Mytheme { //略 public function init() { //略 //コントローラーのルーティング $this->route_controller(); //略 } //略 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; } } } テンプレート決定ロジックに対応した、自動ルーティングの実装
例えば、
page-mypage.php がテンプレートとして表示される場合には、自動的に MT_Page_Mypage_Controller が、
single-ebook.php がテンプレートとして表示される場合には、自動的に MT_Single_Ebook_Controller が
インスタンス化され、xxx_action() メソッドが実行される -
- ●実行されるアクションメソッドは、表示しようとしている投稿のスラッグによって決定され、該当するものがなければ、index_action() となる
-
<?php /** * マイページ処理クラス * * @category includes / controller * @package mytheme * @author AD5 */ class MT_Page_Mypage_Controller { private $auth; public function init() { //ログイン認証 $this->auth = new MT_Auth(); if ( ! $this->auth->is_author_signed_in() ) { wp_safe_redirect( MT_Utility::get_pagelink( MT_Const::PAGE_SIGNIN ) ); } } /** * ACTION * マイページトップ(投稿一覧) */ public function index_action() { $args['post_type'] = 'ebook'; $args['post_status'] = array( 'publish', 'pending', 'draft', 'trash' ); $args['author'] = $this->auth->get_userid(); $args['posts_per_page'] = 20; $args['paged'] = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;; $query = new WP_Query( $args ); return array( 'view' => 'index', 'query' => $query ); } /** * ACTION * レポート新規投稿 */ public function add_action() { if( ! empty( $_POST ) ) { //略 } return array( 'view' => 'add', 'form' => $form, 'categories' => $categories, 'tags' => $tags ); } //略 } 各アクションメソッドは、戻り値として、テンプレートで使用するデータの配列を返す
-
- ●コアクラスが受け取り、properties プロパティに格納し、テンプレートで以下のように値を受け取る
-
$MT->get('profile');
こうすることで、コントローラクラスに分離することができる
-
《デプロイの容易なWordPressテーマ構築》
- ●テーマの機能が成立しないというようなプラグインは、テーマに組み込んでしまう
-
class Mytheme { //略 public function init() { //略 //Built-Inプラグインのロード include_once( TEMPLATEPATH . '/lib/acf/acf.php' ); include_once( TEMPLATEPATH . '/lib/acf-repeater/acf-repeater.php' ); //略 } //略 } プラグインファイルをテーマのサブディレクトリに入れ、メインファイルを include すれば、大抵のプラグインは動く
※但し、管理画面から更新通知が来ない
-
- ●Advanced Custom Fields のフィールド設定はフック用クラスで register_field() を用いて設定
-
<?php /** * 投稿タイプ、タクソノミー、カスタムフィールド * リライトルール、テンプレートルールなどの設定を行う * * @category includes / hooks * @package fresale * @author AD5 */ class MT_Setting { public function init() { //略 add_action( 'init', array( $this, 'set_post_types' ) ); add_action( 'init', array( $this, 'set_taxonomies' ) ); add_action( 'init', array( $this, 'set_custom_fields' ) ); //略 } //略 /** * ACTION HOOK : init * Advanced Custom Fieldsを用いてカスタムフィールドを登録 */ public function set_custom_fields() { if(function_exists( "register_field_group" ) ) { register_field_group( array ( 'id' => 'acf_post_type_ebook', 'title' => 'post_type_ebook', 'fields' => array ( array ( 'key' => 'field_post_type_ebook_pdf', 'label' => 'PDFファイル', 'name' => 'pdf', 'type' => 'file', 'required' => 1, 'save_format' => 'url', 'library' => 'all', ), array ( 'key' => 'field_post_type_ebook_thumbnail', 'label' => 'サムネイル画像', 'name' => 'thumbnail', 'type' => 'image', 'save_format' => 'object', 'preview_size' => 'thumbnail', 'library' => 'all', ), array ( 'key' => 'field_post_type_ebook_catchcopy', 'label' => 'キャッチコピー', 'name' => 'catchcopy', 'type' => 'text', 'default_value' => '', 'placeholder' => '', 'prepend' => '', 'append' => '', 'formatting' => 'html', 'maxlength' => '', ), //略 ), 'location' => array ( array ( array ( 'param' => 'post_type', 'operator' => '==', 'value' => 'ebook', 'order_no' => 0, 'group_no' => 0, ), ), ), 'options' => array ( 'position' => 'normal', 'layout' => 'no_box', 'hide_on_screen' => array ( 0 => 'the_content', 1 => 'featured_image', 2 => 'categories', 3 => 'tags', 4 => 'send-trackbacks', ), ), 'menu_order' => 0, ) ); } //略 } }
-
- ●カスタム投稿タイプやカスタムタクソノミーの設定
-
/** * ACTION HOOK : init * register_post_type() を実行し、投稿タイプを登録 */ public function set_post_types() { $labels = array( 'name' => 'eBook', 'singular_name' => 'eBook', 'menu_name' => 'eBook', ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array( 'slug' => 'ebook' ), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 6, 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' ) ); register_post_type( ebook, $args ); //略 } /** * ACTION HOOK : init * register_taxonomy(), register_taxonomy_for_object_type() を実行し、タクソノミーを追加 */ public function set_taxonomies() { register_taxonomy( 'ebook-genre', 'ebook', array( 'label' => 'レポートカテゴリ', 'show_in_quick_edit' => true, 'show_admin_column' => true, 'rewrite' => array( 'slug' => 'ebook/genre' ), 'hierarchical' => true ) ); register_taxonomy_for_object_type( 'ebook-genre', 'ebook' ); //略 }
-
- ●テーマに必須の固定ページ、テーマの有効化時にフックによって投稿する
-
<?php /** * テーマアクティベート時のフック用クラス * * @category includes / hooks * @package fresale * @author AD5 */ class MT_Activate { public function init() { add_action( "init", array( $this, "do_action" ) ); add_action( "mt_theme_activate", array( $this, "insert_pages" ) ); //略 } /** * ACTION HOOK : init * テーマアクティベート時にカスタムアクションを実行 */ public function do_action() { global $pagenow; if( is_admin() && $pagenow == "themes.php" && isset( $_GET["activated"] ) ) { do_action( "mt_theme_activate" ); } } /** * ACTION HOOK : mt_theme_activate (custom) * 必要な固定ページがなければデフォルト値投稿 */ public function insert_pages() { $default_pages = $this->get_default_pages(); $parent_pages = array(); $args = array( 'hierarchical' => 0, 'post_status' => 'publish' ); $exist_pages = get_pages( $args ); foreach ( $exist_pages as $exist_page ) { $key = $exist_page->post_name; if ( array_key_exists( $key, $default_pages ) ) { unset( $default_pages[$key] ); } $parent_pages[$exist_page->post_name] = $exist_page->ID; } if ( $default_pages ) { foreach ( $default_pages as $default_page ) { $default_page['post_status'] = 'publish'; $default_page['post_type'] = 'page'; if ( array_key_exists( 'post_parent', $default_page ) ) { if ( array_key_exists( $default_page['post_parent'], $parent_pages ) ) { $default_page['post_parent'] = $parent_pages[$default_page['post_parent']]; } else { $default_page['post_parent'] = 0; } } $inserted = wp_insert_post( $default_page ); $parent_pages[$default_page['post_name']] = $inserted; } } } public function get_default_pages() { return array( FS_Const::PAGE_INQUIRY => array( 'post_name' => FS_Const::PAGE_INQUIRY, 'post_title' => 'お問合せ', 'post_content' => 'お問合せ' ), FS_Const::PAGE_REGISTER => array( 'post_name' => FS_Const::PAGE_REGISTER, 'post_title' => '会員登録', 'post_content' => 'レポートを投稿したい方は、こちらから会員登録してください。' ), FS_Const::PAGE_SIGNIN => array( 'post_name' => FS_Const::PAGE_SIGNIN, 'post_title' => 'ログイン', 'post_content' => 'ログイン' ), FS_Const::PAGE_SIGNOUT => array( 'post_name' => FS_Const::PAGE_SIGNOUT, 'post_title' => 'ログアウト', 'post_content' => 'ログアウト', 'post_parent' => FS_Const::PAGE_SIGNIN ), FS_Const::PAGE_MYPAGE => array( 'post_name' => FS_Const::PAGE_MYPAGE, 'post_title' => 'マイページ', 'post_content' => 'マイページ', ), FS_Const::PAGE_MYPAGE_PROFILE => array( 'post_name' => FS_Const::PAGE_MYPAGE_PROFILE, 'post_title' => 'プロフィール編集', 'post_content' => 'プロフィール編集', 'post_parent' => FS_Const::PAGE_MYPAGE, ), ); } //略 } 特にコンテンツを記述せず、専用のページテンプレートを用意して表示を行うようなものは、上記のようにテーマの有効化時にフックによって投稿する
これで、基本的に修正デプロイ時は、ファイルをアップロードしてテーマを再有効化するのみで済む
※リライトルールに関わる処理を追加・修正した時は、パーマリンクのリフレッシュも行われる
※ソース管理のみで完結するので、非常に構成管理が楽になる
※前提として、ソース内でIDによる投稿やタームの参照はしないので、ソースから参照するスラッグは定数化しておくこと