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

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Июл-2020 20:31

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

SPL



Источники
GitHub — MelonPHP
Flutter
MAUI
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_php, #_php, #_php7, #_framework, #_generators, #_codegeneration, #_ui, #_php
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 25-Ноя 20:19
Часовой пояс: UTC + 5