Организация кода в файле functions.php вашей темы WordPress
Чем больше кастомизаций я делаю в WordPress, тем больше задумываюсь о том, стоит ли организовывать этот файл или разделять его на части.
В частности, если у меня есть набор пользовательских функций, которые применяются только к админ-панели, и другие, которые применяются только к публичной части сайта, есть ли смысл выделить все админ-функции в отдельный файл или сгруппировать их вместе?
Ускорит ли разделение их на отдельные файлы или группировка работу сайта WordPress, или WordPress/PHP автоматически пропускает функции, которые имеют префикс is_admin в коде?
Как лучше всего работать с большим файлом функций (мой состоит из 1370 строк).

Если вы достигли точки, когда код в файле functions.php
вашей темы начинает вас перегружать, я определенно рекомендую рассмотреть возможность его разделения на несколько файлов. Я уже настолько привык к этому, что делаю это практически автоматически.
Используйте подключаемые файлы в functions.php
вашей темы
Я создаю подкаталог с названием "includes" в директории темы и разделяю код на подключаемые файлы, организованные по логике, которая мне кажется уместной (что означает, что я постоянно рефакторю и перемещаю код по мере развития сайта). Также я редко размещаю основной код в functions.php
; всё идёт в подключаемые файлы — это просто моё предпочтение.
Вот пример из моего тестового окружения, которое я использую для проверки ответов на WordPress Answers. Каждый раз, отвечая на вопрос, я сохраняю код на случай, если он понадобится снова. Это не совсем то, что вы будете делать для рабочего сайта, но демонстрирует механику разделения кода:
<?php
/*
* functions.php
*
*/
require_once( __DIR__ . '/includes/null-meta-compare.php');
require_once( __DIR__ . '/includes/older-examples.php');
require_once( __DIR__ . '/includes/wp-admin-menu-classes.php');
require_once( __DIR__ . '/includes/admin-menu-function-examples.php');
// WA: Добавление фильтра таксономии в список админки для произвольного типа записи?
// http://wordpress.stackexchange.com/questions/578/
require_once( __DIR__ . '/includes/cpt-filtering-in-admin.php');
require_once( __DIR__ . '/includes/category-fields.php');
require_once( __DIR__ . '/includes/post-list-shortcode.php');
require_once( __DIR__ . '/includes/car-type-urls.php');
require_once( __DIR__ . '/includes/buffer-all.php');
require_once( __DIR__ . '/includes/get-page-selector.php');
// http://wordpress.stackexchange.com/questions/907/
require_once( __DIR__ . '/includes/top-5-posts-per-category.php');
// http://wordpress.stackexchange.com/questions/951/
require_once( __DIR__ . '/includes/alternate-category-metabox.php');
// http://lists.automattic.com/pipermail/wp-hackers/2010-August/034384.html
require_once( __DIR__ . '/includes/remove-status.php');
// http://wordpress.stackexchange.com/questions/1027/removing-the-your-backup-folder-might-be-visible-to-the-public-message-generate
require_once( __DIR__ . '/includes/301-redirects.php');
Или создавайте плагины
Другой вариант — группировать код по функционалу и создавать собственные плагины. Лично я начинаю писать код в functions.php
темы, но к моменту завершения разработки большая часть кода уже переносится в плагины.
Однако организация кода в PHP не даёт значительного прироста производительности
Структурирование PHP-файлов на 99% направлено на порядок и поддерживаемость и лишь на 1% — на производительность (организация файлов .js
и .css
, загружаемых браузером через HTTP, — совсем другая история и имеет серьёзные последствия для производительности). А вот то, как вы организуете PHP-код на сервере, практически не влияет на производительность.
Организация кода — это вопрос личных предпочтений
И последнее: организация кода — это личное предпочтение. Кому-то может не понравиться, как организую код я, так же как и мне может не понравиться чей-то подход. Найдите то, что подходит вам, и придерживайтесь этого, но позвольте вашей стратегии развиваться со временем по мере обучения и накопления опыта.

Хороший ответ, я как раз подошел к моменту, когда нужно разделить файл функций. Как вы думаете, когда стоит переходить от functions.php к плагину. Вы сказали в своем ответе: к тому времени, когда я полностью проработаю код, большая часть моего кода уже перемещается в плагины. Я не до конца понимаю, что вы имеете в виду под "fully проработаю".

+1 за "или создавайте плагины". Если точнее, "плагины для функциональности"

использование относительных путей может быть ненадежным в некоторых настройках, всегда следует использовать абсолютные пути

@MarkKaplun - Вы абсолютно правы. С момента написания этого ответа я усвоил этот урок на собственном горьком опыте. Я собираюсь обновить свой ответ. Спасибо, что указали на это.

Я получаю ошибку "Use of undefined constant DIR - assumed 'DIR' в C:\wamp\www\site\wp-content\themes\mytheme\functions.php" - PHP v5.6.25 и PHP v7.0.10 - Я не могу правильно отформатировать эту DIR в комментарии (двойноеподчеркиваниеDIRдвойноеподчеркивание), но это работает с dirname(двойноеподчеркиваниеFILEдвойноеподчеркивание)

Внимание: вам нужно использовать __DIR__
вместо __DIR___
, иначе вы получите Внутреннюю Ошибку Сервера (500).

Поздний ответ
Как правильно подключать файлы:
function wpse1403_bootstrap()
{
// Здесь мы загружаем из нашей директории includes
// Учитываются как родительские, так и дочерние темы
locate_template( array( 'inc/foo.class.php' ), true, true );
}
add_action( 'after_setup_theme', 'wpse1403_bootstrap' );
То же самое работает и для плагинов.
Как получить правильный путь или URI
Также обратите внимание на функции API файловой системы, такие как:
home_url()
plugin_dir_url()
plugin_dir_path()
admin_url()
get_template_directory()
get_template_directory_uri()
get_stylesheet_directory()
get_stylesheet_directory_uri()
- и другие.
Как уменьшить количество include/require
Если вам нужно загрузить все файлы из директории, используйте:
foreach ( glob( 'path/to/folder/*.php' ) as $file )
include $file;
Имейте в виду, что это игнорирует ошибки (может быть полезно для продакшена)/незагружаемые файлы.
Чтобы изменить это поведение, вы можете использовать другую конфигурацию во время разработки:
$files = ( defined( 'WP_DEBUG' ) AND WP_DEBUG )
? glob( 'path/to/folder/*.php', GLOB_ERR )
: glob( 'path/to/folder/*.php' )
foreach ( $files as $file )
include $file;
Редактирование: ООП/SPL подход
Вернувшись и увидев, что этот ответ получает все больше голосов, я решил показать, как я делаю это сейчас - в мире PHP 5.3+. Следующий пример загружает все файлы из подпапки темы с именем src/
. Здесь находятся мои библиотеки, которые обрабатывают определенные задачи, такие как меню, изображения и т.д. Вам даже не нужно заботиться о имени, так как каждый файл загружается. Если у вас есть другие подпапки в этой директории, они игнорируются.
\FilesystemIterator
- это замена \DirectoryIterator
в PHP 5.3+. Оба являются частью PHP SPL. В то время как в PHP 5.2 можно было отключить встроенное расширение SPL (менее 1% установок делали это), сейчас SPL является частью ядра PHP.
<?php
namespace Theme;
$files = new \FilesystemIterator( __DIR__.'/src', \FilesystemIterator::SKIP_DOTS );
foreach ( $files as $file )
{
/** @noinspection PhpIncludeInspection */
! $files->isDir() and include $files->getRealPath();
}
Ранее, когда я еще поддерживал PHP 5.2.x, я использовал следующее решение: \FilterIterator
в директории src/Filters
для получения только файлов (и не точек указателей папок) и \DirectoryIterator
для цикла и загрузки.
namespace Theme;
use Theme\Filters\IncludesFilter;
$files = new IncludesFilter( new \DirectoryIterator( __DIR__.'/src' ) );
foreach ( $files as $file )
{
include_once $files->current()->getRealPath();
}
\FilterIterator
был очень простым:
<?php
namespace Theme\Filters;
class IncludesFilter extends \FilterIterator
{
public function accept()
{
return
! $this->current()->isDot()
and $this->current()->isFile()
and $this->current()->isReadable();
}
}
Помимо того, что PHP 5.2 уже устарел (как и 5.3), есть тот факт, что это больше кода и еще один файл в игре, поэтому нет причин использовать этот подход и поддерживать PHP 5.2.x.
Итог
РЕДАКТИРОВАНИЕ Очевидно, правильный способ - использовать код с namespace
, подготовленный для автозагрузки PSR-4, размещая все в соответствующей директории, которая уже определена через пространство имен. Затем просто используйте Composer и composer.json
для управления зависимостями и позвольте ему автоматически собрать ваш PHP автозагрузчик (который автоматически импортирует файл при простом вызове use \<namespace>\ClassName
). Это де-факто стандарт в мире PHP, самый простой способ и еще более автоматизированный и упрощенный с помощью WP Starter.

Мне нравится использовать функцию для загрузки файлов из папки. Такой подход упрощает добавление новых возможностей при добавлении новых файлов. Но я всегда пишу в классах или с использованием пространств имен - это дает больше контроля над пространством имен функций, методов и т.д.
Ниже небольшой пример; также можно использовать соглашение об именовании class*.php
public function __construct() {
$this->load_classes();
}
/**
* Возвращает массив возможностей,
* сканирует подпапку плагина "/classes"
*
* @since 0.1
* @return void
*/
protected function load_classes() {
// загружаем все файлы с шаблоном class-*.php из директории classes
foreach( glob( dirname( __FILE__ ) . '/classes/class-*.php' ) as $class )
require_once $class;
}
В темах я часто использую другой сценарий. Я определяю функцию внешнего файла в ID поддержки, см. пример. Это полезно, если я хочу легко отключить функционал внешнего файла. Я использую встроенную функцию WP require_if_theme_supports()
, которая загружает файл только если активен соответствующий ID поддержки. В следующем примере я определяю этот ID поддержки в строке перед загрузкой файла.
/**
* Добавляем поддержку Theme Customizer
*
* @since 09/06/2012
*/
add_theme_support( 'documentation_customizer', array( 'all' ) );
// Подключаем файл кастомайзера тем, если тема поддерживает
require_if_theme_supports(
'documentation_customizer',
get_template_directory() . '/inc/theme-customize.php'
);
Больше примеров можно увидеть в репозитории этой темы.

Что касается разделения функционала, в моей базовой заготовке я использую пользовательскую функцию для поиска папки functions в директории темы. Если папка отсутствует, функция создаёт её. Затем формируется массив всех найденных в этой папке .php файлов (если они есть) и для каждого из них выполняется include().
Таким образом, каждый раз, когда мне нужно добавить новый функционал, я просто добавляю PHP-файл в папку functions, не беспокоясь о внедрении кода в основной файл темы.
<?php
/*
ФУНКЦИИ для автоматического подключения PHP-файлов из папки functions.
*/
//если используется PHP4, создаём функцию scandir
if (!function_exists('scandir')) {
function scandir($directory, $sorting_order = 0) {
$dh = opendir($directory);
while (false !== ($filename = readdir($dh))) {
$files[] = $filename;
}
if ($sorting_order == 0) {
sort($files);
} else {
rsort($files);
}
return ($files);
}
}
/*
* Эта функция возвращает путь к папке functions.
* Если папка не существует, она создаётся.
*/
function get_function_directory_extension($template_url = FALSE) {
//получаем URL шаблона, если он не передан
if (!$template_url)$template_url = get_bloginfo('template_directory');
//заменяем слеши на точки для explode
$template_url_no_slash = str_replace('/', '.', $template_url);
//создаём массив из URL
$template_url_array = explode('.', $template_url_no_slash);
//--разделяем массив
//Вычисляем смещение (нам нужны только последние три уровня)
//Это необходимо для получения правильной директории, а не той, которую передаёт сервер, так как scandir не работает с алиасами.
$offset = count($template_url_array) - 3;
//разделяем массив, оставляя только путь до корневой папки WP (где находится wp-config.php, откуда выполняется фронтенд)
$template_url_array = array_splice($template_url_array, $offset, 3);
//собираем обратно в строку
$template_url_return_string = implode('/', $template_url_array);
fb::log($template_url_return_string, 'Template'); //firephp
//создаём текущую рабочую директорию с расширением шаблона и папкой functions
//если это админка, выходим из папки админки перед сохранением рабочей директории, затем возвращаемся обратно
if (is_admin()) {
$admin_directory = getcwd();
chdir("..");
$current_working_directory = getcwd();
chdir($admin_directory);
} else {
$current_working_directory = getcwd();
}
fb::log($current_working_directory, 'Directory'); //firephp
//альтернативный метод, если chdir не работает на вашем сервере (некоторые Windows-серверы могут его не поддерживать)
//if (is_admin()) $current_working_directory = str_replace('/wp-admin','',$current_working_directory);
$function_folder = $current_working_directory . '/' . $template_url_return_string . '/functions';
if (!is_dir($function_folder)) mkdir($function_folder); //создаём папку, если она ещё не существует (лениво, но удобно... вроде)
//возвращаем путь
return $function_folder;
}
//удаляем элементы массива, не имеющие расширения .php
function only_php_files($scan_dir_list = false) {
if (!$scan_dir_list || !is_array($scan_dir_list)) return false; //если элемент не передан или не является массивом, выходим из функции
foreach ($scan_dir_list as $key => $value) {
if (!strpos($value, '.php')) {
unset($scan_dir_list[$key]);
}
}
return $scan_dir_list;
}
//выполняем функции для создания папки functions, её выбора,
//сканирования, фильтрации только PHP-файлов и их подключения
add_action('wp_head', fetch_php_docs_from_functions_folder(), 1);
function fetch_php_docs_from_functions_folder() {
//получаем директорию functions
$functions_dir = get_function_directory_extension();
//сканируем директорию и удаляем не-PHP файлы
$all_php_docs = only_php_files(scandir($functions_dir));
//подключаем PHP-файлы
if (is_array($all_php_docs)) {
foreach ($all_php_docs as $include) {
include($functions_dir . '/' . $include);
}
}
}

@mildfuzz: Хороший трюк. Лично я не стал бы использовать это в продакшн-коде, потому что он выполняет на каждой загрузке страницы то, что мы могли бы легко сделать один раз при запуске сайта. Также я бы добавил возможность исключать файлы, например, не загружать ничего, что начинается с подчеркивания, чтобы можно было хранить работы в процессе разработки в директории темы. В остальном — отлично!

Отличная идея, но я согласен, что это может привести к ненужной загрузке при каждом запросе. Есть ли простой способ автоматически кэшировать итоговый файл functions.php с возможностью обновления при добавлении новых файлов или через определенные интервалы времени?

Хорошо, но это приводит к негибкости. Также что произойдет, если злоумышленнику удастся поместить свой код туда? И что, если порядок подключения файлов важен?

@MikeSchinkel Я просто называю свои рабочие файлы foo._php, а затем убираю _php, когда хочу, чтобы они запустились.

@kaiser, Полагаю, можно сделать это с помощью cron-скриптов, запускающих функцию, которая выполняет поиск по папкам, как описано выше, но записывает результаты в БД/текстовый файл, а затем основывает загрузки на этой функции. Однако это может привести к тому, что незавершенная работа тоже попадет в загрузку.

@MildFuzz: Майк только что указал мне на API транзиентов. Возможно, это могло бы стать частью какого-то решения...

основываясь на решении, предложенном @mildfuzz - как вы считаете, какой будет лучший метод для автоматического исключения любого файла или папки (и их подфайлов/подпапок) из включения в его подход с авто-включением? Я думаю, можно использовать подход с подчеркиванием в качестве префикса. Какой правильный код будет лучшим решением для реализации такой возможности?

Я управляю сайтом с примерно 50 уникальными пользовательскими типами страниц на нескольких языках в рамках сетевой установки. Вместе с ОГРОМНЫМ количеством плагинов.
Нам пришлось в какой-то момент разделить всё это. Файл functions.php с 20-30 тысячами строк кода — это совсем не весело.
Мы решили полностью переработать весь код, чтобы лучше управлять кодовой базой. Стандартная структура темы WordPress хороша для небольших сайтов, но не для крупных.
Наш новый functions.php содержит только то, что необходимо для запуска сайта, но ничего, что относится к конкретной странице.
Структура темы, которую мы используем сейчас, похожа на шаблон проектирования MVC, но в процедурном стиле программирования.
Например, наша страница участника:
page-member.php. Отвечает за инициализацию страницы. Вызывает нужные AJAX-функции или подобное. Может быть эквивалентом части Controller в стиле MVC.
functions-member.php. Содержит все функции, связанные с этой страницей. Также включается в другие страницы, которым нужны функции для наших участников.
content-member.php. Подготавливает данные для HTML. Может быть эквивалентом Model в MVC.
layout-member.php. HTML-часть.
После внесения этих изменений время разработки сократилось минимум на 50%, и теперь владельцу продукта сложно давать нам новые задачи. :)

Чтобы сделать это более полезным, вы могли бы рассмотреть демонстрацию того, как на самом деле работает этот шаблон MVC.

Мне также было бы интересно увидеть пример вашего подхода, желательно с некоторыми деталями/различными ситуациями. Подход звучит очень интересно. Вы сравнивали нагрузку на сервер/производительность со стандартной методологией, которую используют другие? Если возможно, предоставьте пример на GitHub.

locate_template()
имеет третий параметр …

Я объединил ответы @kaiser и @mikeschinkel.
Все мои кастомизации темы находятся в папке /includes
, внутри которой всё разбито на подпапки.
Я хочу, чтобы только /includes/admin
и её содержимое подключались, когда true === is_admin()
Если папка исключена в iterator_check_traversal_callback
путём возврата false
, то её поддиректории не будут перебираться (или передаваться в iterator_check_traversal_callback
)
/**
* Подключаем все кастомизации из /includes
*/
$includes_import_root =
new \RecursiveDirectoryIterator( __DIR__ . '/includes', \FilesystemIterator::SKIP_DOTS );
function iterator_check_traversal_callback( $current, $key, $iterator ) {
$file_name = $current->getFilename();
// Подключаем только *.php файлы
if ( ! $current->isDir() ) {
return preg_match( '/^.+\.php$/i', $file_name );
}
// Не включаем папку /includes/admin на публичной части сайта
return 'admin' === $file_name
? is_admin()
: true;
}
$iterator_filter = new \RecursiveCallbackFilterIterator(
$includes_import_root, 'iterator_check_traversal_callback'
);
foreach ( new \RecursiveIteratorIterator( $iterator_filter ) as $file ) {
include $file->getRealPath();
}
