<?php
/**
* Clarkson Core Templates.
*/
namespace Clarkson_Core;
use Twig\Extra\Html\HtmlExtension;
use Twig\Extra\Intl\IntlExtension;
use Twig\Extra\Markdown\MarkdownExtension;
use Twig\Extra\String\StringExtension;
/**
* Allows rendering of specific templates with Twig.
*/
class Templates {
/**
* The template types, are all seperate `get_query_template` can be called with.
*
* @see https://developer.wordpress.org/reference/functions/get_query_template/ The code defines the possible `type` values.
*/
private const TEMPLATE_TYPES = array(
'index',
'404',
'archive',
'author',
'category',
'tag',
'taxonomy',
'date',
'embed',
'home',
'frontpage',
'privacypolicy',
'page',
'paged',
'search',
'single',
'singular',
'attachment',
);
/**
* The template context generator.
*
* This object can be used if you want to remove any of the default add_filters.
*
* @var \Clarkson_Core\Template_Context
*/
public $template_context;
/**
* Define has_been_called
*
* @var bool $has_been_called Check if template rendering has already been called.
*/
protected $has_been_called = false;
/**
* The twig environment.
*
* @var null|\Twig\Environment $twig The reusable twig environment object.
*/
private $twig;
/**
* Render template.
*
* @param string $path Post meta _wp_page_template.
* @param array $objects Post objects.
* @param bool $ignore_warning Ignore multiple render warning.
* @internal
*/
public function render( $path, $objects, $ignore_warning = false ):void {
$this->echo_twig( $path, $objects, $ignore_warning );
exit();
}
/**
* Render template using Twig.
*
* @param string $path Post meta _wp_page_template.
* @param array $objects Post objects.
* @param bool $ignore_warning Ignore multiple render warning.
*
* @return string
*/
public function render_twig( $path, $objects, $ignore_warning = false ) {
$clean_path = realpath( $path );
if ( $clean_path ) {
$path = $clean_path;
}
// Twig arguments.
if ( ! $ignore_warning && $this->has_been_called ) {
user_error( 'Template rendering has already been called. If you are trying to render a partial, include the file from the parent template for performance reasons. If you have a specific reason to render multiple times, set ignore_warning to true.', E_USER_NOTICE );
}
$this->has_been_called = true;
$template_dirs = $this->get_templates_dirs();
$template_file = str_replace( $template_dirs, '', $path ); // Retrieve only the path to the template file, relative from the yourtheme/templates directory.
$twig = $this->get_twig_environment( $template_dirs );
/**
* Context variables that are available in twig templates.
*
* @hook clarkson_context_args
* @since 0.1.0
* @param {array} $context Available variables for the twig template.
* @return {array} Available variables for the twig template.
*
* @example
* // We can make the WooCommerce cart available on each template.
* add_filter( 'clarkson_context_args', function( $context ) {
* $context['cart'] = wc()->cart;
* return $context;
* } );
*/
$context_args = apply_filters( 'clarkson_context_args', $objects );
return $twig->render( $template_file, $context_args );
}
private function get_twig_environment( array $template_dirs ):\Twig\Environment {
if ( ! $this->twig ) {
$debug = ( defined( 'WP_DEBUG' ) ? constant( 'WP_DEBUG' ) : false );
$twig_args = array(
'debug' => $debug,
);
/**
* Allows manipulation of the twig envirionment settings.
*
* @hook clarkson_twig_args
* @since 0.1.0
* @param {array} $twig_args Default options to use when instantiating a twig environment.
* @return {array} Options to pass to the twig environment
* @see https://twig.symfony.com/doc/2.x/api.html#environment-options
*
* @example
* // Enable caching in the twig environment.
* add_filter( 'clarkson_twig_args', function( $twig_args ) {
* $twig_args['cache'] = get_stylesheet_directory() . '/dist/template_cache/';
* return $twig_args;
* } );
*/
$twig_args = apply_filters( 'clarkson_twig_args', $twig_args );
$twig_fs = new \Twig\Loader\FilesystemLoader( $template_dirs );
$twig = new \Twig\Environment( $twig_fs, $twig_args );
$twig->addExtension( new Twig_Extension() );
$twig->addExtension( new IntlExtension() );
$twig->addExtension( new StringExtension() );
$twig->addExtension( new HtmlExtension() );
$twig->addExtension( new MarkdownExtension() );
if ( $debug ) {
$twig->addExtension( new \Twig\Extension\DebugExtension() );
}
/**
* Allows themes and plugins to edit the Clarkson Twig environment.
*
* @hook clarkson_twig_environment
* @since 1.0.0
* @param {TwigEnvironment} $twig Twig environment.
* @return {TwigEnvironment} Twig environment.
*
* @example
* // We can add custom twig extensions.
* add_filter( 'clarkson_twig_environment', function( $twig ) {
* $twig->addExtension( new \Custom_Twig_Extension() );
* return $twig;
* } );
*/
$this->twig = apply_filters( 'clarkson_twig_environment', $twig );
}
return $this->twig;
}
/**
* Echo template.
*
* see: https://github.com/level-level/Clarkson-Core/issues/126.
*
* @param string $template_file Post meta _wp_page_template.
* @param array $objects Post objects.
* @param bool $ignore_warning Ignore multiple render warning.
*/
public function echo_twig( $template_file, $objects, $ignore_warning = false ):void {
echo $this->render_twig( $template_file, $objects, $ignore_warning );
}
/**
* Get the template directories where the Twig files are located in.
*
* This takes notices of the child / parent hierarchy, so that's why the child theme gets searched first and then the parent theme, just like the regular WordPress templating hierarchy.
*/
public function get_templates_dirs():array {
$template_dirs = array(
$this->get_stylesheet_dir(),
$this->get_template_dir(),
);
/**
* Add/modify directories that contain twig templates that Clarkson should look for.
*
* @hook clarkson_twig_template_dirs
* @since 0.1.8
* @param {array} $template_dirs Available variables for the twig template.
* @return {array} Template directories to look through.
*
* @example
* // We can add a specific new directory to load templates from to twig.
* add_filter( 'clarkson_twig_template_dirs', function( $template_dirs ) {
* $template_dirs[] = realpath( get_stylesheet_directory() ) . '/admin_templates';
* return $template_dirs;
* } );
*/
$template_dirs = apply_filters( 'clarkson_twig_template_dirs', $template_dirs );
// if no child-theme is used, then these two above are the same.
$template_dirs = array_unique( $template_dirs );
// Ignore template dir if it doesn't exist
$template_dirs = array_filter(
$template_dirs,
function( $template_dir ) {
return file_exists( $template_dir );
}
);
return $template_dirs;
}
/**
* Retrieves the parent theme directory Clarkson Core is using to find templates.
*/
public function get_template_dir():string {
/**
* Modify the template directory path.
*
* @hook clarkson_twig_template_dir
* @since 0.1.0
* @param {string} $template_dir Path to the template directory.
* @return {string} Path to template directory.
*
* @example
* // It is possible to customise the place twig looks for templates.
* add_filter( 'clarkson_twig_template_dir', function( $template_dir ) {
* return get_template_directory() . '/twig_templates';
* } );
*/
return (string) realpath( apply_filters( 'clarkson_twig_template_dir', get_template_directory() . '/templates' ) );
}
/**
* Gets the stylesheet directory Clarkson Core is using to find twig templates.
*/
public function get_stylesheet_dir():string {
/**
* Modify the template directory path for the stylesheet directory.
*
* @hook clarkson_twig_stylesheet_dir
* @since 0.1.8
* @param {string} $stylesheet_dir Path to the stylesheet template directory.
* @return {string} Path to template directory.
*
* @example
* // It is possible to customise the place twig looks for templates.
* add_filter( 'clarkson_twig_stylesheet_dir', function() {
* return get_stylesheet_directory() . '/twig_templates';
* } );
*/
return (string) realpath( apply_filters( 'clarkson_twig_stylesheet_dir', get_stylesheet_directory() . '/templates' ) );
}
/**
* Check template to include.
*
* @param string $template the template.
*
* @return string $template the checked template.
* @internal
*/
public function template_include( $template ) {
global $wp_query;
$extension = pathinfo( $template, PATHINFO_EXTENSION );
if ( 'twig' === $extension ) {
/**
* Add and modify variables available during the template rendering.
*
* @hook clarkson_core_template_context
* @since 1.0.0
* @param {array} $context The context that will be passed onto the template.
* @param {WP_Query} $wp_query The current query that is being rendered.
* @return {array} The context that will be passed onto the template.
*
* @example
* // It is possible to add custom variables to the twig context.
* add_filter( 'clarkson_core_template_context', function( $context, $wp_query ) {
* if( $wp_query->is_tax ){
* $context['tax_variable'] = true;
* }
* return $context
* } );
*/
$context = apply_filters( 'clarkson_core_template_context', array(), $wp_query );
$this->render( $template, $context, true );
}
return $template;
}
/**
* Get templates.
*
* @param array $choices Choices.
*
* @return array
* @internal
*/
public function get_templates( $choices = array() ) {
$templates = wp_cache_get( 'templates', 'clarkson_core' );
if ( is_array( $templates ) ) {
return $templates;
}
$templates = $choices;
$page_templates = array();
foreach ( $this->get_template_files() as $name => $path ) {
if ( preg_match( '#^template-#i', $name ) === 1 ) {
$name = str_replace( 'template-', '', $name );
$name = str_replace( '-', ' ', $name );
$name = ucwords( $name );
/**
* Rename templates shown in the page template dropdown.
*
* @hook clarkson_core_template_name
* @since 1.2.0
* @param {string} $name The name calculated by Clarkson Core from the filename.
* @param {string} $filename The filename of the template being renamed.
* @return {string} The new name for the template.
*
* @example
* // It is possible to change a template name with the filter.
* add_filter( 'clarkson_core_template_name', function( string $name, string $filename ): string {
* if( $filename === 'template-contact.twig' ){
* return __( 'Contact and location', 'textdomain' );
* }
* return $name;
* } );
*/
$name = apply_filters( 'clarkson_core_template_name', $name, basename( $path ) );
$page_templates[ basename( $path ) ] = $name;
}
}
// Now add our template to the list of templates by merging our templates with the existing templates array from the cache.
$templates = array_merge( $templates, $page_templates );
wp_cache_set( 'templates', $templates, 'clarkson_core' );
return $templates;
}
/**
* Adds our templates to the page dropdown for WP v4.7+.
*
* @param array $post_templates Templates array.
* @param string $theme Theme.
* @param object $post Post.
* @param string $post_type Post type.
*
* @return array
* @internal
*/
public function add_new_template( $post_templates, $theme, $post, $post_type ) {
$custom_post_templates = $this->get_templates();
foreach ( $custom_post_templates as $path => $name ) {
$filename = basename( $path );
/**
* Manipulate which post_types get a template in the template dropdown.
*
* @hook clarkson_core_templates_types_for_{$name}
* @since 0.2.1
* @param {string[]} $post_types Which post types the template can be chosen on. By default showns only on 'page'.
* @return {string[]} Post type slugs that show the template in the template dropdown.
*
* @example
* // Show a custom template in the 'post' post-type template dropdown.
* add_filter( 'clarkson_core_templates_types_for_template-sponsored_post.twig', function($post_types){
* $post_types[] = 'post';
* return $post_types;
* } );
*/
$show_on_post_types = apply_filters( 'clarkson_core_templates_types_for_' . $filename, array( 'page' ) );
if ( in_array( $post_type, $show_on_post_types, true ) ) {
$post_templates[ $path ] = $name;
}
}
/**
* Manipulate which post_types get a template in the template dropdown.
*
* @hook clarkson_core_{$post_type}_templates
* @since 0.2.1
* @param {string[]} $post_templates Which post types the template can be chosen on.
* @param {WP_Theme} $theme The theme posts are shown on
* @param {WP_Post|null} $post The current post.
* @param {string} $post_type The post type (already known from hook).
* @return {string[]} Template files as key with their display name as value.
*
* @example
* // Show a custom template in the 'sponsored_post' post-type template dropdown.
* add_filter( 'clarkson_core_sponsored_post_templates.twig', function($post_templates){
* $post_templates['template-example.twig'] = 'Example template';
* return $post_templates;
* } );
*/
return apply_filters( 'clarkson_core_' . $post_type . '_templates', $post_templates, $theme, $post, $post_type );
}
/**
* Add template filters.
*/
private function get_template_files():array {
// Get template files.
$template_paths = $this->get_templates_dirs();
/**
* @hook clarkson_core_template_paths
* @since 0.1.0
* @param {string[]} $template_paths Template paths (stylesheet, parent theme template directory).
* @return {string[]} Template paths that twig will use to load page templates.
*/
apply_filters( 'clarkson_core_template_paths', $template_paths );
$templates = array();
foreach ( $template_paths as $template_path ) {
$templates = array_merge( $templates, $this->get_templates_from_path( $template_path ) );
}
foreach ( $templates as $template ) {
$base = basename( $template );
$base = str_replace( '.twig', '', $base );
$templates[ $base ] = $template;
}
return $templates;
}
/**
* Get templates from path.
*
* @param string $path File path.
*
* @return array
*/
private function get_templates_from_path( string $path ) {
if ( ! $path || ! file_exists( $path ) ) {
return array();
}
$files = glob( "{$path}/template-*.twig" );
if ( empty( $files ) ) {
return array();
}
return $files;
}
/**
* Singleton.
*
* @var Templates|null instance Templates.
*/
protected static $instance = null;
/**
* Get instance.
*
* @return Templates
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new Templates();
}
return self::$instance;
}
public function add_twig_to_template_hierarchy( array $original_templates ):array {
$templates = array();
$directories = array_unique(
array(
str_replace( realpath( get_stylesheet_directory() ), '', $this->get_stylesheet_dir() ),
str_replace( realpath( get_template_directory() ), '', $this->get_template_dir() ),
)
);
foreach ( $original_templates as $template ) {
$pathinfo = pathinfo( $template );
foreach ( $directories as $directory ) {
$twig_template = $pathinfo['dirname'] . $directory . '/' . $pathinfo['filename'] . '.twig';
$templates[] = $twig_template;
}
$templates[] = $template;
}
return $templates;
}
/**
* Clarkson_Core_Templates constructor.
*/
protected function __construct() {
foreach ( self::TEMPLATE_TYPES as $template_type ) {
add_filter( $template_type . '_template_hierarchy', array( $this, 'add_twig_to_template_hierarchy' ), 999 );
}
$this->template_context = new Template_Context();
$this->template_context->register_hooks();
add_action( 'template_include', array( $this, 'template_include' ) );
add_filter( 'acf/location/rule_values/page_template', array( $this, 'get_templates' ) );
// Add a filter to the WP 4.7 version attributes meta box.
// Add filters for all post_types.
add_action(
'wp_loaded',
function() {
$custom_post_types = get_post_types(
array(
'public' => false,
'_builtin' => false,
),
'names',
'or'
);
$builtin_post_types = get_post_types(
array(
'public' => false,
'_builtin' => true,
),
'names',
'or'
);
$post_types = array_merge( $custom_post_types, $builtin_post_types );
foreach ( $post_types as $post_type ) {
if ( is_string( $post_type ) ) {
add_filter( 'theme_' . $post_type . '_templates', array( $this, 'add_new_template' ), 10, 4 );
}
}
}
);
}
/**
* Clone.
*/
private function __clone() {
}
/**
* Wakeup.
*/
public function __wakeup() {
}
}