Как стать автором
Обновить

Наследование шаблонов в ванильном PHP за 35 строк кода?

Время на прочтение4 мин
Количество просмотров5.3K

Попал мне как-то под руку проект на WordPress (WP), где понадобилось сделать кастомную тему. В WP шаблоны нативные, что хорошо, - не надо учить дополнительный язык. Но очень захотелось понаследовать шаблоны как в Twig, а PHP из коробки так не умеет.

Осталось решить вопрос наследования. После изучения проблемы, вдохновляться было решено библиотекой phpti, в которой нашлось пару моментов, которые очень захотелось поправить:

  • Автор библиотеки большими буквами написал “Every Block is Always Executed!“, т. е. все блоки выполняются, даже если переопределены, и никогда не будут выведены.

  • Второй момент - библиотека под капотом использует анализ стека вызовов, что, как мне кажется, чуть-чуть читерство.

  • Третий момент - хочу ещё быстрее. В библиотеке активно используется ob_start на чём и попробуем сэкономить пару спичек.

Библиотека phpti построена вокруг основной конструкции startblock/endblock и наследования при помощи import в начале файла:

<!-- разметка -->
<?php startblock('blockName') ?>
    <!-- контент блока по умолчанию -->
<?php endblock() ?>
<!-- разметка -->

index.php

<?php include 'layout.php' ?> <!-- указываем родительский шаблон -->

<?php startblock('blockName') ?>
    <!-- контент блока -->
<?php endblock() ?>

Некоторые наблюдения:

  • Вызовы вроде start/end можно заменить на анонимную функцию. Это избавит от необходимости сопоставлять вложенные старты и энды, а также сделает код контента ленивым.

  • Родительский шаблон указывается сверху файла. Это значит, что придётся сначала отрабатывать родительский, а потом дочерний шаблон. А значит придётся много чего буферизировать. А почему бы родителя не указать в конце?

  • Можно заметить, что на самом деле есть два разных типа блоков: одни определяются внутри шаблонов куда и выводят результат по месту, а другие по месту определения ничего не выводят, а только переопределяют родительские блоки.

С учётом вышесказанного, переделаем конструкцию на следующую:

layout.php

// 
<!-- разметка -->
<?php slot('blockName', function(){ ?>
    <!-- контент блока по умолчанию -->
<?php }) ?>
<!-- разметка -->

index.php

<?php block('blockName', function(){ ?>
    <!-- контент блока -->
<?php }) ?>

<?php include 'layout.php' ?> <!-- указываем родительский шаблон -->

Разделив блоки на slot и block, библиотеке больше не нужно пытаться понять, когда нужно выводить результат, а когда нужно только переопределить блок.

Вроде выглядит не слишком монструозно по сравнению с оригинальной структурой. Посмотрим, покроет ли такой подход все хотелки.

root.php - базовый шаблон, внизу иерархии:

<!DOCTYPE html>
<html>
  <head>
    <title>
        <!-- 'слот' - место в шаблоне для вывода блоков -->
        <?php slot('title') ?>
    </title>
  </head>
  <body>
    <div id="root">
      <!-- Ещё один слот, теперь с контентом по дефолту -->
      <?php slot('body', function () { ?>
        <p>'body' :: root.php</p>
      <?php }) ?>
    </div>
  </body>
</html>

two-columns.php - промежуточный шаблон:

<?php 
block('title', function () { ?> <!-- 'блок' - контент для вставки в слот -->
  Title :: two-columns.php
<?php });

block('body', function () { ?>
  <div id="two-columnts">
    <div id="main">
      <?php slot('main', function () { ?> <!-- слот внутри блока -->
        <p>'main' :: two-columns.php</p>
      <?php }) ?>
    </div>
    <div id="side">
      <?php slot('side', function () { ?>
        <p>'side' :: two-columns.php</p>
      <?php }) ?>
    </div>
  </div>
  <div id="footer">
    <?php slot('footer', function () { ?>
      <p>'footer' :: two-columns.php</p>
    <?php }) ?>
  </div>
<?php });
include './root.php'; // наследуем от root.php

index.php - страница сайта, верхний шаблон:

<?php
require_once '../src/InheritTpl.php'; 

block('title', function () { ?> 'title' :: index.php <?php });

block('side', function () { ?>
  <p>'side' :: index.php</p>
<?php });

block('main', function () { ?>
  <div id="main-index"> <!-- обернём содержимое от родителя -->
    <?php super() ?> <!-- тут выводим контент из родительского блока -->
  </div>
<?php });

// ещё раз тот же блок, почему бы и нет?
block('main', function () { ?>
  <div id="main-index"> <!-- и ещё раз обернём содержимое -->
    <?php super() ?>
  </div>
<?php });

// а 'footer' пусть остаётся как был

include './two-columns.php';

Результат рендеринга (отформатирован для читабельности):

<!DOCTYPE html>
<html>
  <head>
    <title> 'title' :: index.php </title>
  </head>
  <body>
    <div id="root">
      <div id="two-columnts">
        <div id="main">
          <div id="main-index"> <!-- Обернём содержимое от родителя -->
            <div id="main-index"> <!-- И ещё раз обернём содержимое -->
              <p>'main' :: two-columns.php</p>
            </div>
          </div>
        </div>
        <div id="side">
          <p>'side' :: index.php</p>
        </div>
      </div>
      <div id="footer">
        <p>'footer' :: two-columns.php</p>
      </div>
    </div>
  </body>
</html>

Хотелки наследования удовлетворены. Но вот интересно, удалось ли сделать эту штуку быстрее?

Перепишем пример выше под библиотеку phpti. Дадим ей небольшую фору, т.к. в примере нет тяжеловесных переопределяемых блоков.

Сравнивать будем время 10,000 рендеров на PHP 8.0.2 и процессоре 3.6ГГц.

  • phpti: 0.831 секунд

  • сабж: 0.353 секунд

В качестве вывода можно сказать что размер библиотеки удалось сократить в 10 раз, при этом скорость работы механизма наследования увеличилась как минимум в 2 раза.

Посмотреть исходный код можно тут.

Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+9
Комментарии10

Публикации

Истории

Работа

PHP программист
106 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань