Cocoon設定のname属性をタブ・セクションごとに一覧表示する方法

Cocoonカスタマイズ集
この記事は約28分で読めます。
記事内に広告が含まれています。

はじめに

Cocoon設定画面の項目は非常に多岐にわたります。
スキン制御などを定義する場合、従来はブラウザのデベロッパーツールを開き、膨大なHTMLの中から該当するキー(name属性)を一つひとつ探す必要があり、非常に手間がかかる作業でした。

そこで、Cocoon設定の各項目に対応するname属性を、ダッシュボード上でタブ・セクションごとに一覧表示できるようにしました。

これにより、カスタマイズの際にCocoon設定画面のHTMLを調べることなく、目的の設定項目とname属性をすぐに確認できます。

この記事では、この方法について説明します。

完成イメージ

実装手順

以下のコードをfunctions.phpに追加します。

// 管理画面メニューを追加
add_action('admin_menu', function () {
  add_submenu_page(
    THEME_SETTINGS_PAGE,
    'Cocoon設定name属性一覧',
    'Cocoon設定name属性一覧',
    'manage_options',
    'cocoon_setting_parse',
    'render_cocoon_setting_parse'
  );
}, 99);


// 管理画面用JS・CSSを追加
add_action('admin_enqueue_scripts', function (string $hook_suffix) {
  if (($_GET['page'] ?? '') !== 'cocoon_setting_parse') {
    return;
  }

  ob_start();
  ?>
  .cocoon-setting-parse__tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin: 20px 0 16px;
  }

  .cocoon-setting-parse__tab {
    display: inline-block;
    padding: 6px 12px;
    border: 1px solid #ccc;
    background: #fff;
    color: #333;
    font-size: 13px;
    font-weight: 400;
    cursor: pointer;
  }

  .cocoon-setting-parse__tab:hover {
    background: #f0f0f0;
  }

  .cocoon-setting-parse__tab.is-active {
    background: #2271b1;
    border-color: #2271b1;
    color: #fff;
    font-weight: 400;
  }

  .cocoon-setting-parse__content {
    display: none;
  }

  .cocoon-setting-parse__content.is-active {
    display: block;
  }

  .cocoon-setting-parse__section {
    margin: 20px 0 30px;
  }

  .cocoon-setting-parse__section h2 {
    margin: 0;
    padding: 8px 12px;
  }

  .cocoon-setting-parse__table {
    table-layout: fixed;
    width: 100%;
  }

  .cocoon-setting-parse__table th:nth-child(1),
  .cocoon-setting-parse__table td:nth-child(1) {
    width: 40%;
  }

  .cocoon-setting-parse__table td {
    font-weight: normal;
  }

  .cocoon-setting-parse__name {
    word-break: break-all;
  }
  <?php
  wp_add_inline_style('wp-admin', ob_get_clean());

  ob_start();
  ?>
  jQuery(function($) {
    $('.cocoon-setting-parse__tab').on('click', function () {
      const $tab = $(this);
      const target = $tab.data('target');

      $('.cocoon-setting-parse__tab').removeClass('is-active');
      $tab.addClass('is-active');

      $('.cocoon-setting-parse__content').removeClass('is-active');
      $(target).addClass('is-active');
    });
  });
  <?php
  wp_add_inline_script('jquery', ob_get_clean());
});


/**
 * 解析対象の設定フォーム関数一覧とラベル引数位置を取得
 *
 * @return array<string, array{label_argument: int|null}>
 */
if (!function_exists('get_cocoon_setting_parse_form_functions')):
function get_cocoon_setting_parse_form_functions() {
  return [
    'generate_textbox_tag'      => ['label_argument' => 2],
    'generate_textarea_tag'     => ['label_argument' => 2],
    'generate_selectbox_tag'    => ['label_argument' => 3],
    'generate_checkbox_tag'     => ['label_argument' => 2],
    'generate_radiobox_tag'     => ['label_argument' => null],
    'generate_color_picker_tag' => ['label_argument' => 2],
    'generate_range_tag'        => ['label_argument' => null],
    'generate_number_tag'       => ['label_argument' => null],
    'generate_upload_image_tag' => ['label_argument' => null],
    'generate_upload_file_tag'  => ['label_argument' => null],
    'generate_code_editor_tag'  => ['label_argument' => null],
  ];
}
endif;


/**
 * Cocoonのsettings-tabs.phpのファイルパスを取得
 *
 * @return string ファイルが存在すればパス、存在しなければ空文字
 */
if (!function_exists('get_cocoon_setting_parse_tabs_file')):
function get_cocoon_setting_parse_tabs_file() {
  $file = get_cocoon_template_directory() . '/lib/page-settings/settings-tabs.php';

  if (is_file($file) && is_readable($file)) {
    return $file;
  }

  return '';
}
endif;


/**
 * Cocoon設定タブの配列を取得
 *
 * @return array<string, array> タブ情報の配列
 */
if (!function_exists('get_cocoon_setting_parse_tabs')):
function get_cocoon_setting_parse_tabs() {
  $file = get_cocoon_setting_parse_tabs_file();

  if (!$file) {
    return [];
  }

  if (!function_exists('cocoon_get_settings_tabs')) {
    require_once $file;
  }

  if (!function_exists('cocoon_get_settings_tabs')) {
    return [];
  }

  $tabs = cocoon_get_settings_tabs();

  return is_array($tabs) ? $tabs : [];
}
endif;


/**
 * 指定されたファイルのPHPソースコードを取得
 *
 * @param string $file ファイルパス
 * @return string ソースコード文字列
 */
if (!function_exists('get_cocoon_setting_parse_source')):
function get_cocoon_setting_parse_source(string $file) {
  if (!is_file($file) || !is_readable($file)) {
    return '';
  }

  $source = file_get_contents($file);

  return is_string($source) ? $source : '';
}
endif;


/**
 * PHPの式文字列から評価後の文字列値を抽出
 *
 * @param string $expression 式文字列
 * @return string 評価後の文字列
 */
if (!function_exists('get_cocoon_setting_parse_expression_value')):
function get_cocoon_setting_parse_expression_value(string $expression) {
  $expression = trim($expression);

  if ($expression === '') {
    return '';
  }

  // 文字列連結
  $parts = preg_split('/\s*\.\s*/', $expression);

  if (count($parts) > 1) {
    $value = '';

    foreach ($parts as $part) {
      $part_value = get_cocoon_setting_parse_expression_value($part);

      if ($part_value === '') {
        return '';
      }

      $value .= $part_value;
    }

    return $value;
  }

  // 定数
  if (preg_match('/^([A-Z_][A-Z0-9_]*)$/', $expression, $matches)) {
    if (defined($matches[1])) {
      return (string) constant($matches[1]);
    }

    return $matches[1];
  }

  // 文字列
  if (preg_match('/^[\'"]((?:\\\\.|[^\'"])*)[\'"]$/s', $expression, $matches)) {
    return stripcslashes($matches[1]);
  }

  // 翻訳関数
  if (preg_match('/^(?:__|_e|esc_html__|esc_attr__|esc_html_e|esc_attr_e)\s*\(\s*[\'"]((?:\\\\.|[^\'"])*)[\'"]/s', $expression, $matches)) {
    return stripcslashes($matches[1]);
  }

  // 式中の文字列
  if (preg_match('/[\'"]((?:\\\\.|[^\'"])*)[\'"]/', $expression, $matches)) {
    return stripcslashes($matches[1]);
  }

  return '';
}
endif;


/**
 * 関数呼び出しの括弧から引数一覧を解析して抽出
 *
 * @param string $source ソースコード
 * @param int    $open_position 開始括弧の位置
 * @return array<int, string> 引数の文字列配列
 */
if (!function_exists('get_cocoon_setting_parse_call_arguments')):
function get_cocoon_setting_parse_call_arguments(string $source, int $open_position) {
  $length = strlen($source);
  $depth = 1;
  $quote = '';
  $escape = false;
  $argument = '';
  $arguments = [];

  for ($i = $open_position + 1; $i < $length; $i++) {
    $char = $source[$i];

    if ($quote !== '') {
      $argument .= $char;

      if ($escape) {
        $escape = false;
      } elseif ($char === '\\') {
        $escape = true;
      } elseif ($char === $quote) {
        $quote = '';
      }

      continue;
    }

    if ($char === "'" || $char === '"') {
      $quote = $char;
      $argument .= $char;
      continue;
    }

    if ($char === '(' || $char === '[' || $char === '{') {
      $depth++;
      $argument .= $char;
      continue;
    }

    if ($char === ')' || $char === ']' || $char === '}') {
      $depth--;

      if ($depth === 0) {
        if (trim($argument) !== '') {
          $arguments[] = trim($argument);
        }

        break;
      }

      $argument .= $char;
      continue;
    }

    if ($char === ',' && $depth === 1) {
      $arguments[] = trim($argument);
      $argument = '';
      continue;
    }

    $argument .= $char;
  }

  return $arguments;
}
endif;


/**
 * 関数引数または行ラベルから設定項目名を取得
 *
 * @param string $function_name 関数名
 * @param array  $arguments 引数配列
 * @param string $row_label デフォルトの行ラベル
 * @return string 設定項目名
 */
if (!function_exists('get_cocoon_setting_parse_item_label')):
function get_cocoon_setting_parse_item_label(string $function_name, array $arguments, string $row_label) {
  $functions = get_cocoon_setting_parse_form_functions();

  if (!isset($functions[$function_name])) {
    return $row_label;
  }

  $label_argument = $functions[$function_name]['label_argument'];

  if ($label_argument === null || !isset($arguments[$label_argument])) {
    return $row_label;
  }

  $expression = trim($arguments[$label_argument]);

  // 翻訳関数または文字列を取得
  if (preg_match('/^(?:__|_e|esc_html__|esc_attr__|esc_html_e|esc_attr_e)\s*\(/', $expression) || preg_match('/^[\'"]((?:\\\\.|[^\'"])*)[\'"]$/s', $expression)) {
    $label = get_cocoon_setting_parse_expression_value($expression);

    if ($label !== '') {
      return $label;
    }
  }

  return $row_label;
}
endif;


/**
 * フォームソースから各生成関数を解析して設定項目一覧を取得
 *
 * @param string $source 解析対象のソースコード
 * @param string $row_label 行ラベル
 * @return array<int, array{label: string, name: string}> 項目情報の配列
 */
if (!function_exists('get_cocoon_setting_parse_form_items')):
function get_cocoon_setting_parse_form_items(string $source, string $row_label) {
  $items = [];
  $names = [];
  $functions = get_cocoon_setting_parse_form_functions();

  preg_match_all('/\b(generate_[a-zA-Z0-9_]+_tag)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE);

  foreach ($matches[1] as $match) {
    $function_name = $match[0];
    $function_position = $match[1];

    if (!isset($functions[$function_name])) {
      continue;
    }

    $open_position = strpos($source, '(', $function_position);

    if ($open_position === false) {
      continue;
    }

    $arguments = get_cocoon_setting_parse_call_arguments($source, $open_position);

    if (!isset($arguments[0])) {
      continue;
    }

    // name属性を取得
    $name = get_cocoon_setting_parse_expression_value($arguments[0]);

    if ($name === '' || $name === 'submit') {
      continue;
    }

    // 同一の重複を除外
    if (isset($names[$name])) {
      continue;
    }

    $names[$name] = true;

    // 設定項目名を取得
    $label = get_cocoon_setting_parse_item_label($function_name, $arguments, $row_label);

    $items[] = [
      'label' => $label,
      'name'  => $name,
    ];
  }

  return $items;
}
endif;


/**
 * テーブル行のソースコードからgenerate_label_tagのラベルを取得
 *
 * @param string $row_source 行のソースコード
 * @return string ラベル文字列
 */
if (!function_exists('get_cocoon_setting_parse_row_label')):
function get_cocoon_setting_parse_row_label(string $row_source) {
  if (!preg_match('/\bgenerate_label_tag\s*\(/', $row_source, $match, PREG_OFFSET_CAPTURE)) {
    return '';
  }

  $function_position = $match[0][1];
  $open_position = strpos($row_source, '(', $function_position);

  if ($open_position === false) {
    return '';
  }

  $arguments = get_cocoon_setting_parse_call_arguments($row_source, $open_position);

  if (!isset($arguments[1])) {
    return '';
  }

  return get_cocoon_setting_parse_expression_value($arguments[1]);
}
endif;


/**
 * h2タグの要素内ソースコードからラベル文字を抽出
 *
 * @param string $source h2タグの内部ソースコード
 * @return string 抽出されたラベル
 */
if (!function_exists('get_cocoon_setting_parse_h2_label')):
function get_cocoon_setting_parse_h2_label(string $source) {
  $source = trim($source);

  if ($source === '') {
    return '';
  }

  // 翻訳関数から取得
  if (preg_match('/(?:__|_e|esc_html__|esc_attr__|esc_html_e|esc_attr_e)\s*\(\s*[\'"]((?:\\\\.|[^\'"])*)[\'"]/s', $source, $matches)) {
    return stripcslashes($matches[1]);
  }

  // PHPコードを除去
  $source = preg_replace('/<\?(?:php)?[\s\S]*?\?>/i', '', $source);

  return trim(wp_strip_all_tags($source));
}
endif;


/**
 * ソースコード内のtrタグ単位で設定項目を解析して取得
 *
 * @param string $source 解析対象のソースコード
 * @return array<int, array{label: string, name: string}> 項目情報の配列
 */
if (!function_exists('get_cocoon_setting_parse_rows')):
function get_cocoon_setting_parse_rows(string $source) {
  $items = [];

  preg_match_all('/<tr\b[^>]*>(.*?)<\/tr>/is', $source, $rows);

  foreach ($rows[1] as $row_source) {
    $row_label = get_cocoon_setting_parse_row_label($row_source);
    $row_items = get_cocoon_setting_parse_form_items($row_source, $row_label);

    foreach ($row_items as $item) {
      $items[] = $item;
    }
  }

  return $items;
}
endif;


/**
 * ソースコード内のh2タグ単位でセクション分割し設定項目を取得
 *
 * @param string $source 解析対象のソースコード
 * @return array<int, array{label: string, items: array}> セクション情報の配列
 */
if (!function_exists('get_cocoon_setting_parse_sections')):
function get_cocoon_setting_parse_sections(string $source) {
  $sections = [];

  preg_match_all('/<h2\b[^>]*>(.*?)<\/h2>/is', $source, $matches, PREG_OFFSET_CAPTURE);

  $count = count($matches[0]);

  // h2がない場合
  if ($count === 0) {
    $items = get_cocoon_setting_parse_rows($source);

    if (!empty($items)) {
      $sections[] = [
        'label' => '',
        'items' => $items,
      ];
    }

    return $sections;
  }

  // h2単位で取得
  for ($i = 0; $i < $count; $i++) {
    $h2_source = $matches[1][$i][0];
    $h2_start = $matches[0][$i][1];
    $h2_end = $h2_start + strlen($matches[0][$i][0]);

    $next_h2_start = strlen($source);

    if (isset($matches[0][$i + 1])) {
      $next_h2_start = $matches[0][$i + 1][1];
    }

    $section_source = substr($source, $h2_end, $next_h2_start - $h2_end);
    $h2_label = get_cocoon_setting_parse_h2_label($h2_source);
    $items = get_cocoon_setting_parse_rows($section_source);

    if (empty($items)) {
      continue;
    }

    $sections[] = [
      'label' => $h2_label,
      'items' => $items,
    ];
  }

  return $sections;
}
endif;


/**
 * 指定された設定フォームファイルを解析
 *
 * @param string $file フォームファイルのパス
 * @return array セクション情報の配列
 */
if (!function_exists('parse_cocoon_setting_parse_form')):
function parse_cocoon_setting_parse_form(string $file) {
  $source = get_cocoon_setting_parse_source($file);

  if ($source === '') {
    return [];
  }

  return get_cocoon_setting_parse_sections($source);
}
endif;


/**
 * 管理画面ページを出力
 *
 * @param array $items
 * @return string
 */
if (!function_exists('render_cocoon_setting_parse_table')):
function render_cocoon_setting_parse_table(array $items) {
  if (empty($items)) {
    return '';
  }

  ob_start();
  ?>
  <table class="widefat striped cocoon-setting-parse__table">
    <thead>
      <tr>
        <th>設定項目</th>
        <th>name属性</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ($items as $item): ?>
        <tr>
          <td><?= esc_html($item['label']) ?></td>
          <td class="cocoon-setting-parse__name"><?= esc_html($item['name']) ?></td>
        </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
  <?php
  return ob_get_clean();
}
endif;


/**
 * 単一タブコンテンツのHTMLを生成
 *
 * @param string $tab_id タブID
 * @param array  $tab タブのデータ配列
 * @param string $first_tab_id 先頭タブのID
 * @param string $form_directory フォームディレクトリパス
 * @return string 生成されたHTML文字列
 */
if (!function_exists('render_cocoon_setting_parse_tab_content')):
function render_cocoon_setting_parse_tab_content(string $tab_id, array $tab, string $first_tab_id, string $form_directory) {
  $is_active = ($tab_id === $first_tab_id);
  $active_class = $is_active ? ' is-active' : '';
  $forms = (isset($tab['forms']) && is_array($tab['forms'])) ? $tab['forms'] : [];
  $sections = [];

  foreach ($forms as $form_file) {
    $file = $form_directory . '/' . $form_file;

    if (!is_file($file) || !is_readable($file)) {
      continue;
    }

    $form_sections = parse_cocoon_setting_parse_form($file);

    foreach ($form_sections as $section) {
      if (!empty($section['items'])) {
        $sections[] = $section;
      }
    }
  }

  ob_start();
  ?>
  <div id="cocoon-setting-parse-<?= esc_attr($tab_id) ?>" class="cocoon-setting-parse__content<?= $active_class ?>">
    <?php foreach ($sections as $section): ?>
      <div class="cocoon-setting-parse__section">
        <?php if ($section['label'] !== ''): ?>
          <h2 class="hndle"><?= esc_html($section['label']) ?></h2>
        <?php endif; ?>
        <?= render_cocoon_setting_parse_table($section['items']) ?>
      </div>
    <?php endforeach; ?>
  </div>
  <?php
  return ob_get_clean();
}
endif;


/**
 * 管理画面の設定ページ全体を表示
 *
 * @return void
 */
if (!function_exists('render_cocoon_setting_parse')):
function render_cocoon_setting_parse() {
  if (!current_user_can('manage_options')) {
    wp_die('このページにアクセスする権限がありません。');
  }

  $tabs_file = get_cocoon_setting_parse_tabs_file();

  if (!$tabs_file) {
    echo '<div class="wrap"><h1>Cocoon設定name属性一覧</h1><div class="notice notice-error"><p>settings-tabs.phpを取得できません。</p></div></div>';
    return;
  }

  $tabs = get_cocoon_setting_parse_tabs();

  if (empty($tabs)) {
    echo '<div class="wrap"><h1>Cocoon設定name属性一覧</h1><div class="notice notice-error"><p>Cocoon設定タブを取得できません。</p></div></div>';
    return;
  }

  $first_tab_id = (string) array_key_first($tabs);
  $form_directory = dirname($tabs_file);

  ob_start();
  ?>
  <div class="wrap">
    <h1>Cocoon設定name属性一覧</h1>

    <div class="cocoon-setting-parse__tabs">
      <?php foreach ($tabs as $tab_id => $tab): ?>
        <?php
        $label = $tab['label'] ?? $tab_id;
        $active_class = ($tab_id === $first_tab_id) ? ' is-active' : '';
        ?>
        <button type="button" class="cocoon-setting-parse__tab<?= $active_class ?>" data-target="#cocoon-setting-parse-<?= esc_attr($tab_id) ?>">
          <?= esc_html($label) ?>
        </button>
      <?php endforeach; ?>
    </div>

    <?php foreach ($tabs as $tab_id => $tab): ?>
      <?= render_cocoon_setting_parse_tab_content((string)$tab_id, $tab, $first_tab_id, $form_directory) ?>
    <?php endforeach; ?>
  </div>
  <?php
  echo ob_get_clean();
}
endif;

さいごに

今回紹介した方法を使えば、ブラウザのデベロッパーツールを行ったり来たりするストレスから解放され、スキン制御やテーマカスタマイズのスピードが格段に向上します。

膨大な設定項目が一覧化されることで、開発時のタイピングミスを防ぎ、より正確なコードの実装が可能になります。

タイトルとURLをコピーしました