feat: Replace Twig with native PHP templates
All checks were successful
Create Release Package / build-release (push) Successful in 55s

- Remove twig/twig dependency from composer.json
- Convert all 25 Twig templates to native PHP templates
- New render() method in Plugin.php using PHP include with output buffering
- New render_partial() helper method for including partials
- Templates support theme overrides via fedistream/ directory
- Reduced plugin size by eliminating Twig and its dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 22:48:10 +01:00
parent 6e45b0b6f1
commit 379fd23be0
57 changed files with 1928 additions and 1559 deletions

View File

@@ -24,7 +24,7 @@ use WP_FediStream\PostTypes\Track;
use WP_FediStream\PostTypes\Playlist;
use WP_FediStream\Taxonomies\Genre;
use WP_FediStream\Taxonomies\Mood;
use WP_FediStream\Taxonomies\License;
use WP_FediStream\Taxonomies\License as LicenseTaxonomy;
use WP_FediStream\User\Library as UserLibrary;
use WP_FediStream\User\LibraryPage;
use WP_FediStream\User\Notifications;
@@ -50,21 +50,14 @@ final class Plugin {
private static ?Plugin $instance = null;
/**
* Twig environment instance.
*
* @var \Twig\Environment|null
*/
private ?\Twig\Environment $twig = null;
/**
* Current Twig render depth to prevent infinite recursion.
* Current render depth to prevent infinite recursion.
*
* @var int
*/
private static int $render_depth = 0;
/**
* Maximum allowed Twig render depth.
* Maximum allowed render depth.
* Set to 2 to allow one level of nested includes but prevent deeper recursion.
*
* @var int
@@ -109,7 +102,6 @@ final class Plugin {
* Private constructor to enforce singleton pattern.
*/
private function __construct() {
$this->init_twig();
$this->init_components();
$this->init_hooks();
$this->load_textdomain();
@@ -132,32 +124,6 @@ final class Plugin {
throw new \Exception( 'Cannot unserialize singleton' );
}
/**
* Initialize Twig template engine.
*
* @return void
*/
private function init_twig(): void {
$loader = new \Twig\Loader\FilesystemLoader( WP_FEDISTREAM_PATH . 'templates' );
$this->twig = new \Twig\Environment(
$loader,
array(
'cache' => WP_FEDISTREAM_PATH . 'cache/twig',
'auto_reload' => WP_DEBUG,
)
);
// Add WordPress escaping functions.
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) );
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_js', 'esc_js' ) );
$this->twig->addFunction( new \Twig\TwigFunction( 'wp_nonce_field', 'wp_nonce_field', array( 'is_safe' => array( 'html' ) ) ) );
$this->twig->addFunction( new \Twig\TwigFunction( '__', '__' ) );
$this->twig->addFunction( new \Twig\TwigFunction( '_e', '_e' ) );
}
/**
* Initialize plugin components.
*
@@ -173,7 +139,7 @@ final class Plugin {
// Initialize taxonomies.
$this->taxonomies['genre'] = new Genre();
$this->taxonomies['mood'] = new Mood();
$this->taxonomies['license'] = new License();
$this->taxonomies['license'] = new LicenseTaxonomy();
// Initialize admin components.
if ( is_admin() ) {
@@ -876,19 +842,30 @@ final class Plugin {
}
/**
* Get Twig environment.
* Get the path to a template file.
*
* @return \Twig\Environment
* Checks theme for override first, then falls back to plugin template.
*
* @param string $template Template name (without extension).
* @return string Full path to template file.
*/
public function get_twig(): \Twig\Environment {
return $this->twig;
public function get_template_path( string $template ): string {
// Check theme for override.
$theme_template = locate_template( "fedistream/{$template}.php" );
if ( $theme_template ) {
return $theme_template;
}
// Use plugin template.
return WP_FEDISTREAM_PATH . "templates/{$template}.php";
}
/**
* Render a Twig template.
* Render a PHP template.
*
* @param string $template Template name (without .twig extension).
* @param array $context Template context variables.
* @param string $template Template name (without extension).
* @param array $context Template context variables.
* @param bool $is_main_template Whether this is the main page template.
* @return string Rendered template.
*/
public function render( string $template, array $context = array(), bool $is_main_template = false ): string {
@@ -897,7 +874,7 @@ final class Plugin {
return '<!-- FediStream: blocked during main template render -->';
}
// Prevent infinite recursion in Twig rendering.
// Prevent infinite recursion in rendering.
if ( self::$render_depth >= self::MAX_RENDER_DEPTH ) {
return '<!-- FediStream: render depth exceeded -->';
}
@@ -911,7 +888,24 @@ final class Plugin {
++self::$render_depth;
try {
$result = $this->twig->render( $template . '.twig', $context );
$template_path = $this->get_template_path( $template );
if ( ! file_exists( $template_path ) ) {
throw new \Exception( "Template not found: {$template}" );
}
// Extract context variables for use in template.
// phpcs:ignore WordPress.PHP.DontExtract.extract_extract
extract( $context, EXTR_SKIP );
// Start output buffering.
ob_start();
// Include the template.
include $template_path;
// Get the rendered content.
$result = ob_get_clean();
} finally {
--self::$render_depth;
if ( $is_main_template ) {
@@ -922,6 +916,17 @@ final class Plugin {
return $result;
}
/**
* Render a partial template (helper for use within templates).
*
* @param string $partial Partial template name (without extension).
* @param array $context Template context variables.
* @return string Rendered partial.
*/
public function render_partial( string $partial, array $context = array() ): string {
return $this->render( $partial, $context, false );
}
/**
* Get a post type instance.
*