Русский
Русский
English
Статистика
Реклама

Из песочницы Как я писал кодогенератор на PHP и что из этого получилось

Причины и проблемы, которые нужно было решить


В этой статье я вам расскажу о том как я писал кодогенератор на 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, на котором можно удобно верстать проекты и совмещять верстку с логикой.


На данном фреймворке я написал курсовой проект (скриншоты ниже), использовал его на экзамене и делал на нем учебные задания.


Скриншоты курсового проекта сделанного на MelonPHP




Источники


GitHub MelonPHP


Flutter


MAUI

Источник: habr.com
К списку статей
Опубликовано: 18.07.2020 18:23:19
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Php

Php7

Framework

Generators

Codegeneration

Ui

Категории

Последние комментарии

© 2006-2020, personeltest.ru