ppf-1.2.x-dev/ppf.module
ppf.module
<?php
/**
* @file
* Module file for the Preprocessor Files module.
*
* Allows Drupal to load preprocessor files, similar to how templates are
* discovered and loaded.
*
* e.g. For 'node--page.html.twig', the system will load a respective
* 'node--page.preprocess.php'. Variables preprocessed in that function will
* be available in the respective template. The file functions exactly like a
* traditional preprocess hook.
*/
use Drupal\Core\Url;
use Drupal\ppf\PreprocessorFiles;
/**
* Implements hook_help().
*
* @noinspection PhpUnused
*/
function ppf_help(string $route_name) : ?string {
$settingsUrl = Url::fromRoute('ppf.settings')->toString();
switch ($route_name) {
case 'ppf.settings':
$output = '<p>' . t('The Preprocessor Files module allows you to create dedicated files to preprocess template variables instead of hooks.') . '</p>';
$output .= '<p><strong>' . t('Refer to the official <a href="https://git.drupalcode.org/project/ppf#preprocess-files">README</a> for more detailed documentation!') . '</strong></p>';
return $output;
case 'help.page.ppf':
$output = '<p><strong>' . t('Refer to the official <a href="https://git.drupalcode.org/project/ppf#preprocess-files">README</a> for more detailed documentation!') . '</strong></p>';
$output .= '<p>' . t('The Preprocessor Files module allows you to create dedicated files to preprocess template variables instead of hooks.') . '</p>';
$output .= '<h2>' . t('Quickstart') . '</h2>';
$output .= '<p>' . t('Visit the <a href="@settingsUrl">configurations page</a> to customize the module as you see fit. On the settings page, you can generate a starter folder in your default theme.') . '</p>';
$output .= '<p>' . t('Copy the <code>YOUR_THEME/preprocess/.HOOK.preprocess.php</code> to create a new <code>YOUR_THEME/preprocess/node.preprocess.php</code>.') . '</p>';
$output .= '<p><em>' . t('If you changed the default configurations, adjust the directory name and file extension accordingly.') . '</em></p>';
$output .= '<p>' . t('When visiting a <code>node.html.twig</code> template, you should now be able to see any variables modifications done within your new preprocessor file.') . '</p>';
$output .= '<h2>' . t('Main Functionality') . '</h2>';
$output .= '<p>' . t('This behaviour is similar to <a href="https://www.drupal.org/docs/8/theming-drupal-8/modifying-attributes-in-a-theme-file">Preprocess Functions</a>. Understanding how they work is crucial to the use of this module.') . '</p>';
$output .= '<p>' . t('For example, for the <code>node.html.twig</code> template, you would create a <code>YOUR_THEME_preprocess_node()</code> function in your theme to preprocess template variables.') . '</p>';
$output .= '<p>' . t('With this module, you can create a <code>node.preprocess.php</code> file to preprocess template variables instead.') . '</p>';
$output .= '<p>' . t('This file can be placed in your theme at <code>YOUR_THEME/preprocess/node.preprocess.php</code> and the contents of the file can be treated as if you were in a preprocess hook.') . '</p>';
$output .= '<h2>' . t('Configuration') . '</h2>';
$output .= '<p>' . t('Visit the <a href="@settingsUrl">configurations page</a> to alter settings for the module.', ['@settingsUrl' => $settingsUrl]) . '</p>';
$output .= '<p>' . t('Here you can alter the directory the preprocessor files are loaded in, as well as the file extension for preprocessor files.') . '</p>';
return $output;
}
return NULL;
}
/**
* Implements hook_theme_registry_alter().
*/
function ppf_theme_registry_alter(&$theme_registry) : void {
// Get all files.
$allFiles = [
'theme' => PreprocessorFiles::getPreprocessorFilesForTheme(),
'modules' => PreprocessorFiles::getPreprocessorFilesForActiveModules(),
];
// Loop in our hooks.
foreach ($allFiles as $origin => $files) {
foreach ($files as $hook => $filePaths) {
// If the hook isn't in the registry, we try to find a base hook, so we
// can instantiate it.
if (!isset($theme_registry[$hook])) {
// This aims to emulate the same functionality as preprocess functions.
// e.g. If node--article.html.twig doesn't exist, but a
// HOOK_preprocess_node__article() function exists, a registry entry is
// created for the 'node__article' hook. We need to cover the case where
// a preprocessor file exists, but no template or hook exists.
$baseHookCandidate = explode("__", $hook)[0];
// If no base hook exists, we simply stop here.
if (!isset($theme_registry[$baseHookCandidate])) {
continue;
}
// Now we know a base hook exists.
// We simply want to copy the implementation of the base hook into a new
// entry for our subhook.
$theme_registry[$hook] = $theme_registry[$baseHookCandidate];
// Adjust the data for our subhook.
$theme_registry[$hook]['base hook'] = $baseHookCandidate;
}
// Insert custom preprocess functions for base hook if we need to.
if (isset($theme_registry[$hook]['base hook'])) {
// Insert custom preprocess function of the base hook.
_ppf_insert_hook_preprocess_functions($theme_registry, $hook, TRUE);
}
// Insert custom preprocess functions for hook.
_ppf_insert_hook_preprocess_functions($theme_registry, $hook);
// Entries with base hooks should inherit any preprocessor files.
if (
isset($theme_registry[$hook]['base hook'])
&& isset($theme_registry[$theme_registry[$hook]['base hook']]['ppf']['preprocess files'])
) {
$theme_registry[$hook]['ppf']['base preprocess files'] = $theme_registry[$theme_registry[$hook]['base hook']]['ppf']['preprocess files'];
}
// Add our preprocessor file to the list of files.
foreach ($filePaths as $file) {
$theme_registry[$hook]['ppf']['preprocess files'][$origin][] = $file;
}
}
}
}
/**
* Add custom functions to the 'preprocess functions' entry in the registry.
*
* @param array $theme_registry
* The Drupal theme registry.
* @param string $hook
* The hook to add our custom function to.
* @param bool $base
* Set to TRUE if processing the base hook of the hook.
*/
function _ppf_insert_hook_preprocess_functions(array &$theme_registry, string $hook, bool $base = FALSE) : void {
$themeImplementation = '_ppf_load_hook_theme_preprocessor_files';
$modulesImplementation = '_ppf_load_hook_modules_preprocessor_files';
$check = $hook;
if ($base) {
$themeImplementation = '_ppf_load_base_hook_theme_preprocessor_files';
$modulesImplementation = '_ppf_load_base_hook_modules_preprocessor_files';
$check = $theme_registry[$hook]['base hook'];
}
// For preprocessor files loaded from the theme, execute them last, or after
// the last traditional theme preprocess found.
if (!in_array($themeImplementation, $theme_registry[$hook]['preprocess functions'])) {
if (!$base || !isset($theme_registry[$hook]['base hook'])) {
$theme_registry[$hook]['preprocess functions'][] = $themeImplementation;
}
else {
// Get the active theme and base themes.
$activeTheme = PreprocessorFiles::service()->themeManager->getActiveTheme();
$baseThemes = $activeTheme->getBaseThemeExtensions();
// We'll do some shenanigans here to accomplish this.
$themeList = array_reverse(array_merge([$activeTheme->getName() => $activeTheme], $baseThemes));
// Here we want to find the last theme preprocess function.
$lastThemePreprocess = NULL;
foreach ($themeList as $themeName => $extension) {
$foundThemePreprocess = array_filter($theme_registry[$hook]['preprocess functions'], function ($entry) use ($themeName, $check) {
return str_starts_with($entry, $themeName . '_preprocess') && str_ends_with($entry, 'preprocess_' . $check);
});
if (!empty($foundThemePreprocess)) {
$lastThemePreprocess = end($foundThemePreprocess);
}
}
// Place plugin preprocessing right after theme preprocessing.
// Otherwise, we try something different.
if ($lastThemePreprocess !== NULL) {
$key = array_search($lastThemePreprocess, $theme_registry[$hook]['preprocess functions']);
$theme_registry[$hook]['preprocess functions'] = _ppf_array_insert_after($theme_registry[$hook]['preprocess functions'], $key, [$themeImplementation => $themeImplementation]);
}
else {
$theme_registry[$hook]['preprocess functions'][] = $themeImplementation;
}
}
}
// For preprocessor files loaded from modules, execute them after traditional
// module hooks, but before theme hooks.
if (!in_array($modulesImplementation, $theme_registry[$hook]['preprocess functions'])) {
// We'll do some shenanigans here to accomplish this.
$moduleList = PreprocessorFiles::service()->moduleHandler->getModuleList();
// Here we want to find the last module preprocess function.
$lastModulePreprocess = NULL;
foreach ($moduleList as $moduleName => $extension) {
$foundModulePreprocess = array_filter($theme_registry[$hook]['preprocess functions'], function ($entry) use ($moduleName, $check) {
return str_starts_with($entry, $moduleName . '_preprocess') && str_ends_with($entry, 'preprocess_' . $check);
});
if (!empty($foundModulePreprocess)) {
$lastModulePreprocess = end($foundModulePreprocess);
}
}
// Place plugin preprocessing right before theme preprocessing.
// Otherwise, we try something different.
if ($lastModulePreprocess !== NULL) {
$key = array_search($lastModulePreprocess, $theme_registry[$hook]['preprocess functions']);
$theme_registry[$hook]['preprocess functions'] = _ppf_array_insert_after($theme_registry[$hook]['preprocess functions'], $key, [$modulesImplementation => $modulesImplementation]);
}
else {
// If a module preprocess could not be located, we try to place it
// after the traditional 'template_preprocess'.
// We go further than that if 'template_preprocess_HOOK' exists.
$templatePreprocessKey = array_search('template_preprocess', $theme_registry[$hook]['preprocess functions']);
$templatePreprocessHookKey = array_search('template_preprocess_' . $hook, $theme_registry[$hook]['preprocess functions']);
$templatePreprocessBaseHookKey = FALSE;
if (isset($theme_registry[$hook]['base hook'])) {
$templatePreprocessBaseHookKey = array_search('template_preprocess_' . $hook, $theme_registry[$hook]['preprocess functions']);
}
if ($templatePreprocessHookKey !== FALSE) {
$key = array_search($templatePreprocessHookKey, $theme_registry[$hook]['preprocess functions']);
$theme_registry[$hook]['preprocess functions'] = _ppf_array_insert_after($theme_registry[$hook]['preprocess functions'], $key, [$modulesImplementation => $modulesImplementation]);
}
elseif (isset($theme_registry[$hook]['base hook']) && $templatePreprocessBaseHookKey !== FALSE) {
$key = array_search($templatePreprocessBaseHookKey, $theme_registry[$hook]['preprocess functions']);
$theme_registry[$hook]['preprocess functions'] = _ppf_array_insert_after($theme_registry[$hook]['preprocess functions'], $key, [$modulesImplementation => $modulesImplementation]);
}
else {
$key = array_search($templatePreprocessKey, $theme_registry[$hook]['preprocess functions']);
$theme_registry[$hook]['preprocess functions'] = _ppf_array_insert_after($theme_registry[$hook]['preprocess functions'], $key, [$modulesImplementation => $modulesImplementation]);
}
}
}
}
/**
* Handle loading of relevant preprocessor file for a given template.
*
* @param array $variables
* The array of variables passed to the Twig template.
* @param string $hook
* The theme hook.
* @param array $info
* The theme info.
*/
function _ppf_load_hook_theme_preprocessor_files(array &$variables, string $hook, array $info) : void {
if (isset($info['ppf']['preprocess files']['theme'])) {
foreach ($info['ppf']['preprocess files']['theme'] as $file) {
if (file_exists($file)) {
include $file;
}
}
}
}
/**
* Handle loading of relevant preprocessor file for a given template.
*
* @param array $variables
* The array of variables passed to the Twig template.
* @param string $hook
* The theme hook.
* @param array $info
* The theme info.
*/
function _ppf_load_hook_modules_preprocessor_files(array &$variables, string $hook, array $info) : void {
if (isset($info['ppf']['preprocess files']['modules'])) {
foreach ($info['ppf']['preprocess files']['modules'] as $file) {
if (file_exists($file)) {
include $file;
}
}
}
}
/**
* Handle loading of relevant preprocessor file for a given template.
*
* @param array $variables
* The array of variables passed to the Twig template.
* @param string $hook
* The theme hook.
* @param array $info
* The theme info.
*/
function _ppf_load_base_hook_theme_preprocessor_files(array &$variables, string $hook, array $info) : void {
if (isset($info['ppf']['base preprocess files']['theme'])) {
foreach ($info['ppf']['base preprocess files']['theme'] as $file) {
if (file_exists($file)) {
include $file;
}
}
}
}
/**
* Handle loading of relevant preprocessor file for a given template.
*
* @param array $variables
* The array of variables passed to the Twig template.
* @param string $hook
* The theme hook.
* @param array $info
* The theme info.
*/
function _ppf_load_base_hook_modules_preprocessor_files(array &$variables, string $hook, array $info) : void {
if (isset($info['ppf']['base preprocess files']['modules'])) {
foreach ($info['ppf']['base preprocess files']['modules'] as $file) {
if (file_exists($file)) {
include $file;
}
}
}
}
/**
* Insert a value or key/value pair after a specific key in an array.
*
* If key doesn't exist, value is appended to the end of the array.
*
* @param array $array
* @param string $key
* @param array $new
*
* @return array
*/
function _ppf_array_insert_after(array $array, string $key, array $new) : array {
$keys = array_keys( $array );
$index = array_search( $key, $keys );
$pos = false === $index ? count( $array ) : $index + 1;
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
}
