MVCモデル風WordPressカスタマイズ設計

WordPressを大規模カスタマイズして新規サイトを構築する際、わかりやすい設計を目指すために、MVCの概念を元に設計したものをまとめてみました。
Gutenbergのカスタムブロックに対応しています。
特にWordPressカスタマイズ用pluginの項が肝で、他はおまけのメモです。

# assetsディレクトリ

ルートに設置する（のが好き）。
/assets/内には以下を設置する。

- css/
- fonts/
- images/
- js/
- scss/

後々の変更も考慮して、オリジナルフォント用svgはfonts/svg/に置いておく。

# scssファイル設計

scssファイルは/assets/scss/に設置し、/assets/css/にcssファイルを書き出す。

ディレクトリ構造は吉本式BEM設計に従う。

SCSSのファイル設計について Vol.1 (吉本式BEM設計)
http://tsudoi.org/guide/detail/8.html

# 自作プラグイン

WordPressのカスタマイズについては以下の２つのプラグインにまとめる。

## カスタムブロック用plugin

Create Guten Block Toolkitでサクッと開発環境を構築。
src内は、1ブロック=1ディレクトリで作成する。
カスタムブロック開発について、詳細はまた別でまとめようと思います。

Create Guten BlockでGutenbergのカスタムブロック開発環境を構築する
https://www.webopixel.net/wordpress/1420.html

Create Guten Block Toolkit
https://ahmadawais.com/create-guten-block-toolkit/

【追記】
カスタムブロック開発について別記事にまとめました。
[【Gutenberg】カスタムフィールド職人からカスタムブロック職人へ](https://qiita.com/ichigo_daifuku3/items/81c26edb05cb16d81c9e)

## themeカスタマイズ用plugin

themeカスタマイズ用pluginには、WordPressデフォルトのfunctions.phpをスパゲッティにしないためのhook用ファイルと、各テンプレートファイルのHTMLを見やすくするためのcontrollerファイルを設置する。

### デフォルトのfunctions.phpの位置付け

header, footer用のフック等、全体に関わるもののみを記述する。

小規模なカスタマイズであれば、functions.phpだけで十分だと思います。

### ディレクトリ構造

ファイルは、カスタム投稿タイプごとにディレクトリにまとめる。
1ファイルごとにカプセル化する。

plugin.php
post_type_1/
　├ init.php
　├ setting.php
　├ hooks/
　　　├ post-type.php
　　　├ category.php
　　　├ blocks.php
　├ controllers/
　　　├ utility.php
　　　├ single.php
　　　├ archive.php
　　　├ taxonomy.php
page/
　├ init.php
　├ setting.php
　├ hooks/
　├ controllers/
admin/
　├ init.php
　├ hooks/
　├ css/
　├ js/

hooks/とcontrollers/ 内は必要に応じてファイルを作成。
必要があれば、上記の構成図にないファイルも作成して良い。
例） the_contentにフックさせる関数用のhooks/content.php
例） タクソノミーAのテンプレート用のcontrollers/taxonomy-a.php

### ファイルの説明
簡単な例を用いて、各ファイルの説明を書いておきます。

#### plugin.php  
メインファイル。各カスタム投稿タイプ用init.php呼び出しのみ。

```php:plugin.php(コード例)
<?php
/**
 * Plugin Name: My Theme Customize
 * Plugin URI:
 * Description: themeカスタマイズ用プラグイン
 * Version: 1.0.0
 * Author: hoge
 * Author URI: https://hoge.jp/
 */

defined( 'ABSPATH' ) || die();


/**
 * Post Type 1
 */
require_once plugin_dir_path(__FILE__) . 'post_type_1/init.php';

// 以下略
```

#### post_type_1/ 
あるカスタム投稿タイプ用のディレクトリ。名称は任意。

#### init.php 
add_actionとadd_filterのみ

```php:init.php(コード例)
defined( 'ABSPATH' ) || die();

/**
 * Post Type
 */
require_once(dirname(__FILE__) . '/hooks/post-type.php');
$post_type = new PostTypePostType1();

// Create Post type
add_action('init', array($post_type, 'create_post_type'), 10);

// 以下略


/**
 * Category
 */
require_once(dirname(__FILE__) . '/hooks/category.php');
$category = new CategoryPostType1();

// Create Post type
add_action('init', array($category, 'create_taxonomy'), 10);

// 以下略


/**
 * Blocks
 */
require_once(dirname(__FILE__) . '/hooks/blocks.php');
$blocks = new BlocksPostType1();

// Add Costom Block Category
add_filter('block_categories', array($blocks, 'add_custom_block_category'), 10, 2);

// Allowed Block
add_filter('allowed_block_types', array($blocks, 'custom_allowed_block_types'), 10, 2);

// Register Meta Field
add_action('init', array($blocks, 'register_meta_field'));

// 以下略
```

#### setting.php  
public変数をまとめておく。必要なければ、このファイルはなくても良い。

```php:setting.php(コード例)
if (!class_exists('SettingPostType1')) :
class SettingPostType1 {

	public $post_type = array(
		'type'  => 'post_type_1',
		'label' => '投稿タイプ１',
	);

	public $taxonomy_1 = array(
		'cat'   => 'cat1',
		'label' => 'タクソノミー１',
	);

	public $block_types = array(
		'myplugin/title',
		'myplugin/content',
		'myplugin/image',
	);

	public $meta_fields = array(
		'meta_title'     => array('タイトル', 'string'),
		'meta_content'   => array('説明文', 'string'),
		'meta_image_id'  => array('画像ID', 'integer'),
		'meta_image_url' => array('画像URL', 'string'),
	);

	// 以下略
}
endif;
```

#### hooks/
init.phpで呼び出されるフック用の関数をまとめておくディレクトリ。

#### hooks/post-type.php
register_post_typeなど投稿タイプの設定用関数をまとめる。
例えば、この投稿タイプの投稿を保存した際に、save_postにフックさせる関数などもこのファイルに記述。

```php:hooks/post-type.php(コード例)

require_once(dirname(__FILE__) . '/../setting.php');

if (!class_exists('PostTypePostType1')) :
class PostTypePostType1 extends SettingPostType1 {


	/**
	 * Create Post type
	 */
	public function create_post_type() {

		$template = array(
			array($this->block_types[0], array()),
			array($this->block_types[1], array()),
			array($this->block_types[2], array()),
		);

		register_post_type($this->post_type['type'], array(
			'label'         => $this->post_type['label'],
			'public'        => true,
			'query_var'     => false,
			'menu_position' => 5,
			'taxonomies'    => array($this->taxonomy_1['cat']),
			'supports'      => array('title', 'editor', 'thumbnail', 'author', 'custom-fields', 'page-attributes'),
			'show_in_rest'  => true,
			'template'      => $template,
			'rewrite' => array(
				'with_front' => false
			)
		));

		add_theme_support('post-thumbnails', array($this->post_type['type']));
	}

	// 以下略
}
endif;
```

#### hooks/category.php  
register_taxonomyなどカスタムタクソノミーの設定用関数をまとめる。

```php:hooks/category.php(コード例)

require_once(dirname(__FILE__) . '/../setting.php');

if (!class_exists('CategoryPostType1')) :
class CategoryPostType1 extends SettingPostType1 {


	/**
	 * Create Taxonomy
	 */
	public function create_taxonomy() {

		register_taxonomy(
			$this->taxonomy_1['cat'],
			array($this->post_type['type']),
			array(
				'label'             => $this->taxonomy_1['label'],
				'show_in_rest'      => true,
				'hierarchical'      => true,
				'show_admin_column' => true,
				'rewrite'           => array(
					'with_front' => false
				)
			)
		);
	}

	// 以下略
}
endif;
```

#### hooks/blocks.php  
カスタムブロック用関数をまとめる。

```php:hooks/blocks.php(コード例)

require_once(dirname(__FILE__) . '/../setting.php');

if (!class_exists('BlocksPostType1')) :
class BlocksPostType1 extends SettingPostType1 {


	/**
	 * Add Costom Block Category
	 */
	public function add_custom_block_category($categories, $post) {
		$categories = array_merge(
			array(
				array(
					'slug'  => 'block-' . $this->post_type['type'],
					'title' => $this->post_type['label'] . '用ブロック',
					'icon'  => 'media-document',
				),
			),
			$categories
		);
		return $categories;
	}


	/**
	 * Allowed Block
	 */
	public function custom_allowed_block_types($allowed_block_types, $post) {
		if ($post->post_type === $this->post_type['type']) {

			$allowed_block_types = array_merge($this->block_types, $allowed_block_types);
		}
		return $allowed_block_types;
	}


	/**
	 * Register Meta Field
	 */
	public function register_meta_field() {
		foreach ($this->meta_fields as $key => $lavel) {
			register_meta('post', $key, array(
				'show_in_rest' => true,
				'single'       => true,
				'type'         => $lavel[1]
			));
		}
	}
}
endif;
```

#### controllers/
各テンプレートファイル用のcontrollerをまとめておくディレクトリ。

#### controllers/utility.php
テンプレートファイル用の共通関数用ファイル。settingを継承。

```php:controllers/utility.php(コード例)

require_once(dirname(__FILE__) . '/../setting.php');

if (!class_exists('UtilityPostType1')) :
class UtilityPostType1 extends SettingPostType1 {

	// 以下略
}
endif;
```

#### controllers/single.php  
テンプレートファイルsingle.php用のcontroller、関数。utilityを継承。

```php:controllers/single.php(コード例)

require_once(dirname(__FILE__) . '/utility.php');

if (!class_exists('SinglePostType1')) :
class SinglePostType1 extends UtilityPostType1 {


	private $post_id = null;
	private $meta_data = array();

	public $detail = array();


	public function __construct() {

		parent::__construct();

		$this->post_id = get_the_ID();

		$this->meta_data = get_registered_metadata('post', $this->post_id);

		$this->set_detail_data();
	}


	private function set_detail_data() {

		foreach ($this->meta_fields as $key => $lavel) {

			if (isset($this->detail[$key]) && $this->detail[$key] !== '') {
				continue;
			}

			if ($lavel[1] === 'string') {
				$this->detail[$key] = isset($this->meta_data[$key][0]) ? $this->meta_data[$key][0] : '';
			} else if ($lavel[1] === 'integer') {
				$this->detail[$key] = isset($this->meta_data[$key][0]) ? $this->meta_data[$key][0] : null;
			}
		}

		$this->set_image_tag();
	}


	private function set_image_tag() {

		$this->detail['image_tag'] = '<img src="' . $this->detail['meta_image_url'] . '" alt="' . $this->detail['meta_title'] . '" />';
	}
}
endif;
```

#### single-post_type_1.php  

theme/内のテンプレートファイルsingle-post_type_1.phpのコード例は以下。
定数'CUSTOM_THEME_PLUGIN_DIR'はfunctions.phpで定義しておく。

```php:single-post_type_1.php(コード例)

$ctlClass = new stdClass();
$plugin_file = CUSTOM_THEME_PLUGIN_DIR . 'post_type_1/controllers/single.php';
if (file_exists($plugin_file)) {
	require_once($plugin_file);
	$ctlClass = new SinglePostType1();
}

get_header();
the_post();

$detail = isset($ctlClass->detail) ? $ctlClass->detail : array();
?>
<article>
	<div class="container">
		<h1><?php the_title(); ?></h1>
		<section class="content">
			<?php the_content(); ?>
		</section>
		<section class="image">
			<h2><?php echo $detail['meta_title']; ?></h2>
			<p><?php echo $detail['image_tag']; ?></p>
			<p><?php echo $detail['meta_content']; ?></p>
		</section>
	</div>
</article>
<?php
```

#### controllers/archive.php, controllers/taxonomy.phpなど
上記single.phpと同様。

#### page/
固定ページ用のディレクトリ。

#### admin/
管理画面のカスタマイズ用フックやcss, js。