Причины и проблемы, которые нужно было решить
В этой статье я вам расскажу о том как я писал кодогенератор на php. Расскажу о пути, который он прошел от генерации простых таблиц, до довольно полноценного генератора html и css кода. Приведу примеры его использования и покажу уже сделанные проекты.
В этом семестре на одном из предметов можно было использовать только PHP.
После бесконеного ренейма проекта Проект получил имя MelonPHP. Чтобы люди думали о еде когда произносили его имя? Но у нас тут статья не о генерации бреда, поэтому давайте я вам расскажу о причине его создания.
Написать надо было много, но это не проблема. Основная проблема заключалась в выводе HTML кода через PHP. Я постараюсь объяснить проблему ниже.
Например вот вывод текста через всем знакомое echo:
$text = "out text"; echo "<p>$text</p>";
Выглядит просто и понятно. Давайте возьмем отрезок кода моего друга, где он генерирует таблицу:
... $sql = "SELECT * FROM table";$result = $conn->query($sql); if($result->num_rows > 0) { echo "<b>Table table</b><br><br>"; echo "<table border=2>"; echo "<tr><td> name </td>"."<td> name </td>"."<td> name </td></tr>"; while($row = $result->fetch_assoc()) { echo "<tr><td>".$row["name"]."</td><td>".$row["name"]."</td><td>".$row["name"]."</td></tr>"; } echo "</table>"; } else { echo "0 results";} ...
Это страшный код демонстрирует проблемы, которые я хотел решить:
- Присутствие html в php коде, что делает его по моему мнению менее читаемым. Все-таки файл для одного яп должен содержать код только одного яп(а), по моему мнению
- Нет разделения логики, все в каше. Хотелось более приятный "фронтенд" на PHP
Стоит отметить, что я относительно давно пишу на Flutter и мне очень нравится идея заложенная в его основе, связанная с написанием интерфейса с помощью постройки дерева из виджетов. Я решил позаимствовать оттуда идею с нодами (виджетами).
Я был уверен, что быстрее будет написать небольшой кодогенератор, чем терпеть это.
Изначально генератор занимался генерацией таблиц через функции. Но потом перерос в нечто более масштабное.
Основными идеями были следующими:
- UI пишется из элементов/компонентов (привет React)
- Удобные макеты (Избавиться от div, div, div, div...)
- Чтобы весь UI писался на PHP (без JS, без HTML, без CSS).
Rebuild через callback события, через AJAX + JQueryне сужденоУдобная система роутовне суждено- Поддержка CSS (и не просто строку писать, на уровне "width: 100px", а полноценная поддержка прямо в PHP коде)
- ООП
Особенности MelonPHP
- Почти все элементы (кроме текста, кнопки и еще нескольких) по умолчанию имеют ширину и высоту в 100%, в том числе и документ.
- Кроме того, если элементы будут выходить за пределы страницы, то скролла по умолчанию не будет. Для этого нужно использовать ScrollView.
- Так же по умолчанию нельзя выделать никакие элементы.
Архитектура
Очень печально, что в PHP отсутствует передачу параметров по имени, и по этому я решил использовать вместо них функции. Я уверен что это не менее удобный аналог (Какого было мое удивление, когда Microsoft показали MAUI, который использует ту же идею с функциями).
Все классы в MelonPHP наследуется от Node. Это простой класс, который имеет только 2 функции: Generate(), static Create().
- Generate() возвращает string — сгенерированный код.
- Create() — это статическая функция. Она нужна чтобы было проще создавать ноды в дереве.
abstract class Node { abstract function Generate() : string; static function Create() ... }
Element
Element — это более высокоуровневый класс, который нужен для более комфортного написания своих элементов.
Элемент в основном занимается генерацией чистого html кода.
Элементами в Фреймворк являются такие сущности как контейнер, кнопка, таблица и тд.
Component
Основная идея компонента в том, что этот класс управляет, и состоит из дерева элементов в нем. Компонент наследуется от элемента (бредовая идея).
Компонентами могут быть например дисплеи (аналог страниц в MelonPHP), списки, карточки, навигационные меню и тд.
abstract class Component extends Element { function Initialize() ... abstract function Build() : Element; function Detach() ... }
Компонент удобен тем, что вам не надо генерировать строки; чем занимается Element. Так же в нем есть удобные функции для использования логики.
Попробуем написать простой компонент. Создадим класс ListItem, наследуемый от Component.
Перезапишем функции Initialize() и Build().
Initialize() вызывается при создание компонента. В ней например можно инициализировать переменные или обработать логику.
Build() вызывается при генерации элемента. В ней обязательно должен возвращаться элемент. Обязательная для перезаписи.
Detach() вызывается при удалении компонента.
В Build() возвратим контейнер, а в качестве его ребенка элемент текста и присвоим ему текст из переменной класса $Text.
В Initialize() пропишем значение $Text по умолчанию.
Добавим функцию Text(string) в которой будет записываться значение пользователя в переменную $Text.
Обязательно надо возвращать $this в функциях, которые будут вызываться в дереве.
class ListItem extends Component { private $Text; function Initialize() { $this->Text = "Name"; } function Build() : Element { return Container::Create() ->Child( Text::Create() ->Text($this->Text) ); } function Text(string $string) { $this->Text = $string; return $this; } }
DisplayComponent
DisplayComponent — это компонент, который может выводит сгенерированный код на страницу. Для генерации нужно вызвать функцию Display.
Попробуем написать пример простого дисплея.
В функции Build() возвратим Document и присваиваем ему Title(string).
В DisplayComponent, в функции Build() всегда должен возвращаться Document. Document — это класс, который генерирует стандартную разметку HTML5.
Создадим функцию BuildList(), в которой через цикл заполним колонку созданными выше ListItem.
В качестве ребенка документа вызовем BuildList() функцию. Разделение дерева из нод на функции не дает ему превратиться в макаронного монстра.
Если будет ситуация что надо выполнить какую-то логику прямо в дереве, то для того есть класс Builder. Но так лучше не делать...
После тела класса вызовем функцию Diplay(), которая при переходе на данный файл, на сайте cгенерирует его и выведит.
class ListDisplay extends DisplayComponent { function Build() : Document { return Document::Create() // название страницы ->Title("test page") ->Child($this->BuildList()); } function BuildList() { $column = new Column; for($i = 0; $i < 10; $i++) $column->Children( ListItem::Create() ->Text("number: $i") ); return $column; } } ListDisplay::Display();
Макеты
Одна из целей преследуемых при создании фреймворка — удобство при создание макетов сайта.
Container

Container — это макет, который используется для декоративных целей.
Может содержать только одного ребенка.
Column и Row
У большинства элементов есть дети. Если у элемента доступен метод Child то он может иметь только одного ребенка, а если Children то у него может быть больше одного ребенка.
Так же Child перезаписывает переменную ребенка в то время как Children добавляет аргументы в верх стека.
Если в Children один элемент в аргументе, то не обязательно его заносить в массив.
Тоесть вместо Children([Text::Create()]) можно написать Children(Text::Create())
Column — это макет, который выравнивает его детей вертикально.
Обращаясь к функциям CrossAlign и MainAlign можно выравнивать детей внутри колонки.

Row идентичен Column, но выравнивает детей горизонтально.

Stack
Stack — это макет, дети которого не позиционированы. Это полезно если вы хотите чтобы ноды пересекались, для создания более красивого дизайна.

ScrollView, HorizontalScrollView, VerticalScrollView

Эти контейнеры который является областью для скороллинга.
HorizontalScrollView — в этом контейнере можно скроллить только по горизонтальной оси.
VerticalScrollView — в этом контейнере можно скроллить только по вертикальной оси.
ScrollView — в этом ко��тейнере можно скроллить по всем осям.
Стилизация
Долга думая как лучше встроить css в фреймворк я пришел к идее с константами.
Например у нас в css есть "background-color". Я строку записываю в константу и в php коде можно будет использовать без "". Это намного удобнее.
... const BackgroundBlendMode = "background-blend-mode"; const BackgroundAttachment = "background-attachment"; const Border = "border"; const BorderSpacing = "border-spacing"; const BorderRadius = "border-radius"; const BorderImage = "border-image"; ...
Что касается например такой конструкции "34".Px. Тут идея с константами выглядит не читабельно. По этому я решил в таких ситуациях использовать функции для css — например Px(34). Выглядит понятно и вписывается в пхп код.
Простая стилизация
Для простой стилизации в элементе функция ThemeParameter(...). Первый аргумент — это название параметра, а второй аргумент — это или массив из значений/значение.
Рассмотрим пример.
В первом параметре мы изменим цвет фона на #f0f0f0.
Во втором параметре мы добавим отступы. Сверху и снизу 20px, справа и слева 15px.
Значения в массиве генерируются через пробел. Если вы хотите чтобы генерация происходила через запятую, то для этого есть функция CommaLine().
... Container::Create() ->ThemeParameter(BackgroundColor, Hex("f0f0f0")) ->ThemeParameter(Padding, [Px(20), Px(15)]); ...
Как видно все очень просто и удобно, но если нам понадобятся модификаторы (hover например)? Для этого сделаны темы.
Темы
Темы в этом Фреймворк — это более продвинутый css, с media, keyframes, и модификаторами.
Напишем тему для контейнера с модификатором hover и active.
Для того надо создать класс темы и добавить в него ThemeBlock через метод ThemeBlocks.
Блоку темы нужно присвоить ключ / ключи. Я назову ключ my_container.
Дальше в блок темы можно добавить модификаторы. Я добавил: StandartModifier, HoverModifier, ActiveModifier. И задал для них параметры через метод Parameter(...). Parameter работает так же как ThemeParameter.
function GetMyTheme() : Theme { return Theme::Create() ->ThemeBlocks([ ThemeBlock::Create() ->Keys("my_container") ->Modifiers([ StandartModifier::Create() ->Parameter(BackgroundColor, Red) ->Parameter(Padding, [Px(10), Px(12)]), HoverModifier::Create() ->Parameter(BackgroundColor, Green), ActiveModifier::Create() ->Parameter(BackgroundColor, Blue) ]) ]); }
Дальше контейнеру я присвоил ключ (имя класса в css) темы через метод ThemeKeys. Но для того чтобы тему можно было использовать ее надо добавить в документ через метод Themes.
class TestThemeDisplay extends DisplayComponent { function Build() : Document { return Document::Create() ->Themes(GetMyTheme()) ->Child( Container::Create() ->ThemeKeys("my_container") ); } } TestThemeDisplay::Display();
После можно запустить дисплей и увидеть что тема по ключу применилась.
Продвинутые анимации
Для продвинутой анимации есть keyframes.
Для того чтобы добавить в тему keyframe, используйте метод FrameBlocks.
Добавим уже в существующую тему FrameBlock.
В FrameBlock есть метод Frames. Вызовем его и добавим несколько фреймов, так же для каждого фрейма надо указывать Value. Оно может быть в процентах (используйте функцию Pr(value)) или может быть константой From или To.
Каждому кадру можно добавлять параметры, которые будут уникальны и плавно изменятся между другими кадрами.
Так же добавим блок темы, к которому будет применяться эта анимация и назовем его shake_text.
Создадим текст в предыдущем дисплее, в контейнере и присвоим ему ключ темы.
Так же на примере видно, что в параметры можно использовать и обычные строки.
function GetMyTheme() : Theme { return Theme::Create() ->ThemeBlocks([ ThemeBlock::Create() ->Keys("my_container") ->Modifiers([ StandartModifier::Create() ->Parameter(Padding, [Px(10), Px(12)]), HoverModifier::Create() ->Parameter(BackgroundColor, Green), ActiveModifier::Create() ->Parameter(BackgroundColor, Blue) ]), ThemeBlock::Create() ->Keys("shake_text") ->Modifiers([ StandartModifier::Create() ->Parameter(Color, Red) ->Parameter(Animation, ["shake_text_anim", ".2s", "ease-in-out", "5", "alternate-reverse"]) ]) ]) ->FrameBlocks( FrameBlock::Create() ->Key("shake_text_anim") ->Frames([ Frame::Create() ->Value(Pr(0)) ->Parameter(Transform, Translate(0, 0)), Frame::Create() ->Value(Pr(25)) ->Parameter(Color, Hex("ff4040")) ->Parameter(Filter, Blur(Px(0.5))), Frame::Create() ->Value(Pr(50)) ->Parameter(Filter, Blur(Px(1.2))), Frame::Create() ->Value(Pr(75)) ->Parameter(Color, Hex("ff4040")) ->Parameter(Filter, Blur(Px(0.5))), Frame::Create() ->Value(Pr(100)) ->Parameter(Transform, Translate(Px(10), 0)), ]) ); }
class TestThemeDisplay extends DisplayComponent { function Build() : Document { return Document::Create() ->Themes(GetMyTheme()) ->Child( Container::Create() ->ThemeKeys("my_container") ->Child( Text::Create() ->ThemeKeys("shake_text") ->Text("Error text") ) ); } } TestThemeDisplay::Display();

Адаптивность
Создадим еще 2 темы. Одна будет для мобильных девайсов, а вторая для пк. У темы есть функции: MinWidth, MaxWidth, MinHeight, MaxHeight, объявив которые вы можете указать на каком размере будет работать тема.
Теме для телефонов зададим MinWidth 800px.
Теме для пк зададим MaxWidth 800px.
Создадим блок темы где в стандартном модификаторе для мобильной версии будет присваиваться цвет фона зелёный, а на пк версии — желтый. Назовём блок adaptive_color.
Добавим обе темы в документ дисплея.
Добавим ключи темы к контейнеру.
function GetMobileTheme() : Theme { return Theme::Create() ->MinWidth(Px(800)) ->ThemeBlocks( ThemeBlock::Create() ->Keys("adaptive_color") ->Modifiers( StandartModifier::Create() ->Parameter(BackgroundColor, Green) ) ); }
function GetDesktopTheme() : Theme { return Theme::Create() ->MaxWidth(Px(800)) ->ThemeBlocks( ThemeBlock::Create() ->Keys("adaptive_color") ->Modifiers( StandartModifier::Create() ->Parameter(BackgroundColor, Red) ) ); }
class TestThemeDisplay extends DisplayComponent { function Build() : Document { return Document::Create() ->Themes([ GetMyTheme(), GetDesktopTheme(), GetMobileTheme() ]) ->Child( Container::Create() ->ThemeKeys(["my_container", "adaptive_color"]) ->Child( Text::Create() ->ThemeKeys("shake_text") ->Text("Error text") ) ); } } TestThemeDisplay::Display();

Логика
Попробуем написать простой кликкер.
Для начала нам надо создать класс и наследовать его от DisplayComponent.
Создадим функцию Build() и возвратим в ней Document.
class ClickerDisplay extends DisplayComponent { function Build() : Element { return Document::Create() ->Title("Clicker"); } } ClickerDisplay::Display();
Добавим колонку в качестве ребенка документа.
Так же в качестве детей колонки добавим текст и кнопку.
class ClickerDisplay extends DisplayComponent { function Build() : Element { return Document::Create() ->Title("Clicker") ->Child( Column::Create() ->Children([ Text::Create() ->Text("Pressed 0 times"), Button::Create() ->Text("Press") ]) ); } } ClickerDisplay::Display();
Результат будет следующим.

Далее добавим простые ThemeParameter, чтобы сделать наш пример красивее.
class ClickerDisplay extends DisplayComponent { function Build() : Element { return Document::Create() ->Title("Clicker") ->Child( Column::Create() ->ThemeParameter(Padding, Px(15)) ->Children([ Text::Create() ->ThemeParameter(PaddingBottom, Px(15)) ->Text("Pressed 0 times"), Button::Create() ->ThemeParameter(Width, Auto) ->ThemeParameter(Padding, [Px(4), Px(10)]) ->ThemeParameter(BackgroundColor, Blue) ->ThemeParameter(Color, White) ->ThemeParameter(BorderRadius, Px(4)) ->Text("Press") ]) ); } } ClickerDisplay::Display();
Выглядит куда лучше в несколько простых строчек.

Теперь можно добавить логику.
Для начала нужно инициализировать функцию Initialize() и создать приватную переменную TapCount.
Аналог form в фреймворке — это Action.
Добавим Action в наше дерево элементов. Action тип пусть будет Post. В качестве детей укажем нашу колонку, где находится наша кнопка.
Далее добавим click_count переменную в Action. А в качестве её значение присвоим TapCount.
В Initialize() через Action::GetValue(name, standart_value, action_type) получим наше переменную. В качестве значения по у��олчанию укажем 0, а в качестве типа укажем Post.
Добавим инкремент для нашей переменной.
В тексте выведим "Press $this->TapCount times".
Все, простой кликкер готов.
class ClickerDisplay extends DisplayComponent { private $TapCount; function Initialize() { $this->TapCount = Action::GetValue("click_count", 0 /* standart value */, ActionTypes::Post); $this->TapCount++; } function Build() : Document { return Document::Create() ->Title("Test page") ->Child( Action::Create() ->Type(ActionTypes::Post) ->Variable("click_count", $this->TapCount) ->Child( Column::Create() ->ThemeParameter(Padding, Px(15)) ->Children([ Text::Create() ->ThemeParameter(PaddingBottom, Px(15)) ->Text("Press $this->TapCount times"), Button::Create() ->ThemeParameter(Width, Auto) ->ThemeParameter(Padding, [Px(4), Px(10)]) ->ThemeParameter(BackgroundColor, Blue) ->ThemeParameter(Color, White) ->ThemeParameter(BorderRadius, Px(4)) ->Text("Press") ]) ) ); } } ClickerDisplay::Display();

Итог
Мне удалось написать простой, но достаточно мощный кодогенератор.
Он прошел путь от генерации простых таблиц до полноценного генератора html и css, на котором можно удобно верстать проекты и совмещять верстку с логикой.
На данном фреймворке я написал курсовой проект (скриншоты ниже), использовал его на экзамене и делал на нём учебные задания.



