PHP-DataGen — генератор PHP классов со строго-типизированными свойствами +11



Введение


Помимо многих проблем, в PHP существует проблема строгой типизации переменных и свойств классов, точнее её отсутствие. Более того, нет даже возможности однозначно задать какие будут свойства у объектов того или иного класса, пользуясь только синтаксисом и не прибегая к так называемым магическим методам (потому что любое свойство может быть удалено при помощи оператора unset, а также к объекту может быть дописано несуществующее ранее свойство).

Однако при разработке часто возникает потребность в чётком знании, что можно ожидать от объекта, а чего можно не ожидать. Разумеется, можно пойти простым путём: сделать все свойства protected и понаписать геттеров и сеттеров. Много бойлерплейта, хочется проще. Лично я пытался решить эту проблему с помощью трейтов, но выходило всё равно некрасиво. Так и появилась идея этого проекта…

Кому интересно, добро пожаловать под кат!

Описание


Проект PHP-DataGen[1] является утилитой — генератором кода PHP классов со строго-типизированными свойствами и направлен на упрощение работы PHP программистов. Инструмент имеет как возможность управлять генерацией с помощью PHP скриптов, так и CLI для работы со встроенным парсером собственного языка (далее — PDGL).

Из соображений удобства использования целевой аудиторией (PHP программисты), для разработки был выбран язык PHP.

Версия последнего на данный момент релиза — v0.3-alpha. К моменту выхода стабильного релиза планируется переписать весь «низкокачественный» код (написанный без достаточной квалификации), но пока всё и так работает.

Краткий обзор


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

Проект рассматривается в состоянии коммита 75974bee3b4cccd1af1722acac775d68011f7fa6[2].

CLI


На данный момент PHP-DataGen поддерживает 2 собственные команды: compile и build. Первая используется для поштучной компиляции файлов, вторая для компиляции всех файлов в проекте (директории). Использование команд максимально интуитивное и может быть изучено вручную благодаря библиотеке Symfony Console[6], на которой основан CLI.

Также планируется добавить работу с файлом конфигурации для управления деталями процесса компиляции. При этом будет добавлено считывание файла конфигурации из корня проекта и команда config для удобного изменения конфигурации проекта.

PDGL


До начала разработки было несколько идей по поводу внедрения утилиты в проекты:

  1. Чтение PHPDoc и других комментариев
  2. Введение специальных модификаторов и т.п. в обычный PHP код
  3. Создание собственного языка

Так как изначально проект создавался «по образу и подобию» утилиты moc популярного C++ фреймворка Qt, в приоритете был второй вариант, однако после некоторых раздумий, первые два варианта были отброшены мной как неоправданно сложные для реализации.

PDGL предназначен для описания файлов PHP, которые получаются на выходе PHP-DataGen. Каждый файл можно представить в виде дерева, отдалённо напоминающего абстрактное синтаксическое дерево[3], которое состоит лишь из трёх типов узлов: файл, класс, поле.



Все поддерживающиеся языком операторы представлены в файле schema.md[4] в корне проекта, но без описания, что делает тот или иной оператор. Операторы namespace и use работают также как и в обычном PHP, однако с классом, полями и их модификаторами всё не так просто.

Из модификаторов класса можно выделить лишь один нестандартный для PHP модификатор final, который также имеет вариацию final!. Дело в том, что результат работы PHP-DataGen — класс, который для работы должен быть расширен с помощью другого класса.

Модификатор final превращает класс в готовый для непосредственного использования, путём убирания префикса (по умолчанию, пока что без возможности изменения, Data_) и модификатора abstract итогового PHP класса.

Модификатор final!, который «под капотом» именуется не иначе как «final final» является дополнением к модификатору final (и не может быть использован без него) и добавляет к итоговому PHP классу модификатор final.

Поле класса


Синтаксис поля класса очень мало похож на синтаксис свойств PHP и даже больше, на мой взгляд, напоминает синтаксис свойств классов Kotlin.

Начнём с того, что написано в файле schema.md[4]:

// Field declaration
[direct] <val/var> <Field name>[: <Type name>[, <Validator names>]][ <:/</>= [`[``]]<Default value>[`[``]]];

А теперь по порядку (операторы выделены жирным, подстановки — курсивом):

  • direct — модификатор. При наличии позволяет расширяющему классу обращаться к свойствам напрямую (устанавливает модификатор доступа protected вместо private);
  • val или var — оператор объявления поля. Если используется val — свойство недоступно для редактирования после установки в конструкторе, если var — доступно;
  • Field name — название поля, указывается без характерного для PHP знака доллара ($);
  • : — необязательный оператор двоеточия позволяет указать тип поля. Если не указан — тип поля считается mixed;
  • Type name — название типа. Может быть одним из стандартных типов PHP (без учёта регистра) или названием класса. Если оканчивается знаком вопроса (например, string?), тогда поле может хранить также значение null;
  • , — необязательный оператор запятая позволяет указать после названия типа (или валидатора) также название валидатора;
  • Validator name — название валидатора (см. следующий раздел);
  • <=, := или = — оператор присваивания значения по-умолчанию. В вариации <= присваивает значение при объявлении свойства. В вариации := присваивает значение при вызове конструктора без проверки типа и вызова валидаторов. В вариации = присваивает значение при вызове конструктора с проверкой типа и вызовом валидаторов;
  • ` или ``` — см. Default value;
  • Default value — значение поля по-умолчанию. Может быть окружено операторами ` или ``` при наличии точки с запятой (;) (кроме случаев, когда используется вариация оператора присваивания значения по-умолчанию <=). Нет разницы в использовании ` или ```, если в значении по-умолчанию не присутствует символов обратного апострофа (`), в этом случае необходимо использовать оператор ```.

Валидаторы


Для лучшей фильтрации возможных значений полей планируется ввести возможность добавлять свои валидаторы — функции проверки (или модификации) значения. При том что в коде PHP-DataGen обработка валидаторов присутствует, пока нет способа добавлять их. Это одна из возможностей, которые должны появиться с появлением чтения конфигурации.

Примеры работы


Некоторые примеры работы инструмента можно найти в репозитории. Среди них:
PDGL PHP
app/Type.pdata app/Data_Type.php
app/Model/FieldModel.pdata app/Model/FieldModel.php
tests/Test.pdata tests/Data_Test.php

Примеры приведены в виде ссылок из-за большого объёма кода

Разработка


В самом начале реализации идеи начался длительный «ступор» связанный с отсутствием продуманной архитектуры и, как следствие, неправильно выбранным порядком разработки. Вскоре после появления ясности в голове — зарождения в голове архитектуры будущего инструмента, была начата разработка.

Архитектура


Упрощённая архитектура утилиты
Упрощённая архитектура утилиты

На схеме выше изображена очень упрощённая архитектура PHP-DataGen. Она состоит из четырёх модулей:

  • Parser — модуль, отвечающий за разбор кода;
  • Building* — модуль, содержащий билдеры «сущностей», которыми оперирует компилятор;
  • Models — модуль, содержащий модели «сущностей», которыми оперирует компилятор. Объекты классов этого модуля порождают классы модуля Building;
  • Compiler — модуль, отвечающий за генерацию кода на основе моделей.


* — на схеме опечатка (не Builders, а Building).

Ход разработки


Если до появления цельной архитектуры я (безуспешно) пытался написать Parser, то сразу после её появления я взялся за модуль Building. Затем были написаны модели и Compiler. Таким образом, уже через два дня появился рабочий прототип, позволяющий генерировать код с помощью PHP скрипта.

Далее предстояло написать Parser. Из-за незнания об абстрактных синтаксических деревьях[3], ломать голову пришлось долго. В итоге получился конечный автомат[5], имеющий 3 состояния (которые также состоят из некоторых собственных состояний): FileState, ClassState и FieldState. Каждое из этих состояний при помощи соответствующих билдеров создаёт модели для Compiler.

Использованные при разработке библиотеки и инструменты


В проекте используются следующие библиотеки:

  • Symfony Console[6] — для разработки CLI;
  • Symfony Finder[7] — для поиска файлов при использовании CLI.

Проект разрабатывался при помощи следующих инструментов:

  • Vim — редактор кода;
  • Git — система контроля версий;

Также, при написании статьи, для рисования диаграмм, использовался онлайн сервис Creately[8].

Ссылки


  1. php-datagen — GitHub;
  2. 75974bee3b4cccd1af1722acac775d68011f7fa6 — GitHub;
  3. Абстрактное синтаксическое дерево — Википедия;
  4. Файл schema.md — GitHub;
  5. Теория вычислений. Введение в конечные автоматы — Хабр;
  6. The Console Component — Symfony Docs;
  7. The Finder Component — Symfony Docs;
  8. Creately.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (25):

  1. kruslan
    /#18835379

    Вашу-бы энергию, да в мирных целях…

    • andrew_tch
      /#18836847

      И с нормальным кодом, например PHPParser использовать?

  2. GenkaOk
    /#18835669 / +2

    Как мне кажется всё наоборот усложнили.
    Проще написать свой класс с магическим методом и переменными аля «strVar» «intVar», где в начале тип переменной и наследоваться от него.
    Хотя это в некоторых случаях наоборот помешает разработке.

    Но в чем проблема get/set? Они не так много раздувают кода, а в современных IDE ещё и генерируются сами.
    Всё-таки в языке есть способ для типизации переменных, да он немного замудренный, но позвольте, php с динамической типизацией, что тоже имеет свои плюсы.

    • rjhdby
      /#18837093

      переменными аля «strVar» «intVar»

      эдак и до Венгерской записи докатиться можно

  3. AlexLeonov
    /#18835923 / -1

    Когда хочется писать на C++ и на Java но, извините, толку не хватает — рождаются подобные «проекты»

    Имхо это всё от банального незнания PHP и неумения на нём писать.

    Простите, если кого задел за живое.

  4. zim32
    /#18836031 / -2

    Все равно ошибки в реантайме а не на этапе компиляции. У чем тогда вообщн смысл?

  5. ArthurKushman
    /#18836225 / +2

    Есть проект, если интересно — входные данные RAML, выходные JSON-API (REST), в Laravel инфраструктуре с поддержкой всех компонентов из коробки, в кратце — генерит Controllers/Models/Migrations/Middlwares с поддержкой типов, функциональные тесты (на основе CodeCeption), конфиг модуля, laravel-modules — поддерживает генерацию последующих модулей типа V1, V2, APIv3 итд
    github.com/RJAPI/raml-json-api

    Всегда рад контрибьютам.

  6. rjhdby
    /#18836253 / +4

    Вам бы в статью примеров кода и результата его компиляции.

    А так то удачи вам в вашем начинании! И не слушайте брюзжания зануд — при прочих равных лучше делать, чем не делать. Главное, чтоб вам самому было интересно и полезно (прокачивало навыки), а там, может быть, и люди подтянутся (а может и нет, но это не повод все бросать ;)

    • ProgMiner
      /#18838319

      Спасибо! Идея проекта пришла не спонтанно, и скорее всего инструмент будет использоваться хотя бы в собственных проектах.

      По поводу примеров, тут мой косяк. Добавил абзац со ссылками на примеры из репозитория.

  7. alexeevdv
    /#18837759 / +1

    С типизированием свойств объектов в PHP нет особых проблем. В нормальном ООП у объекта не должно быть никаких публичных свойств, а весь доступ к внутреннему состоянию и его изменение происходит через методы которые прекрасно типизируются в современном PHP. Делая публично доступными свойства объекта мы пренебрегаем такой важной вещью как инкапсуляция

  8. FrankSinatra
    /#18837887

    Почему бы описание синтаксиса элементов класса не вынести в шаблоны? От метода protected function compileClass(ClassModel $classModel, FileModel $fileModel) перехватило дух.
    И не помешало бы натравить PHP Code Sniffer на ваш проект.

    • ProgMiner
      /#18837949

      К сожалению в коде сейчас очень много таких моментов, потому что писалось все в стиле "поскорее, пока не кончился запал".
      В будущем планируется все это вычистить, в первую очередь компилятор и парсер.

  9. Fantyk
    /#18837981 / +1

    Статье очень не хватает «что было на входе» и «что получили после применения инструмента».

    По мне вы в PhpStorm не нашли кнопки Code\Generate.

    • ProgMiner
      /#18838327

      Добавил абзац со ссылками на примеры из репозитория.

      На PhpStorm денег нет, а обманывать отечественного производителя желания нет.

      • rjhdby
        /#18840171

        1) У них есть open source лицензия
        2) Месяц бесплатно, а дальше просто раз в пол-часа перезапуск среды
        3) JetBrains довольно толерантны к нелицензионному использованию. Вплоть до того, что консультировали по проблемам со взломаным софтом на рутрекере (посыл такой — пиратку качают те, кто не может купить. Подсядут, разбогатеют и купят)

        PS третий пункт для полноты картины, а не призыв делать неправильно

        • t_kanstantsin
          /#18842947

          OpenSource есть, но надо регистрироваться как OpenSource — всё же ограничение.
          Но есть ещё минимум 2 возможности: EAP, который доступен 90% времени, и студенческая лицензия

      • alekciy
        /#18840213 / -1

        На PhpStorm денег нет

        У вас нет ~5,7к рублей? Я не верю, что профессиональный разработчик не может выделить из своего бюджета ~500 рублей/месяц в течении года.

        • FrankSinatra
          /#18841485 / +1

          PhpStorm не панацея, а средство и по большому счету вкусовщина, как и Eclipse или NetBeans. А кто-то может довольствоваться и Visual Studio Code, у которого тоже есть плагины для генерации кода.
          Автор же решил задачу по своему и при желании данный проект можно развить, чтобы генерировать не только PHP классы, а поддерживать разные языки.

  10. vlreshet
    /#18841605

    Совсем непонятно, почему в итоговом коде (который сгенерирован):

    1. Отвратительный код-стайл без отступов
    2. Используется змеиная_нотация, хотя в PHP общепринятым является camelCase
    3. Не используется встроенный в язык тайпхинтинг
    4. Нет сеттеров (не, ну реально. геттеры вижу — сеттеров нет. далеко не всегда все поля устанавливаются в конструкторе, и больше не модифицируются)

    ?

    Да и в конце концов — почему не работать поверх PHP кода (написал код — запустил генератор — он дописал класс до нужного состояния). А не вот это вот всё с *.pdata...)

    • ProgMiner
      /#18842001

      1. Отступы нужны для понимая человеком, что по большому счёту не актуально для автоматически генерируемого кода. Конечно функция выравнивания после генерации в планах.
      2. По той же причине, что и в первом пункте, но плюс к этому, чтобы не было неоднозначности, какой метод вызвать при осуществлении доступа к свойству (после префикса get_/set_/validate_ используется точное название свойства с сохранением регистра).
      3. Изначально он был использован, пока не оказалось (для меня это было неожиданно), что типы не поддерживают null. Именно тогда произошёл отказ от тайпхинтинга и появилось разделение nullable и не nullable полей.
      4. Похоже, что вы невнимательно читали описание языка. Сеттеры генерируются для полей объявленных с помощью var.

      Не очень представляю как именно это реализовать, использовать комментарии?

      • oxidmod
        /#18842207

        Изначально он был использован, пока не оказалось (для меня это было неожиданно), что типы не поддерживают null.

        PHP 7.1

      • vlreshet
        /#18842381

        Не очень представляю как именно это реализовать, использовать комментарии?


        Как вариант. Это называется «Аннотации», и большинство современных фреймворков использует такой подход. Выглядит это как-то вот так:
        class Foo
        {
          /**
           * @var integer
           */
          public $bar;
        }


        Конечно функция выравнивания после генерации в планах.

        Очень рекомендую прикрутить готовую библиотеку вроде «php-cs-fixer» вместо велосипеда.

        4. Похоже, что вы невнимательно читали описание языка. Сеттеры генерируются для полей объявленных с помощью var.
        Согласен, упустил.

        • ProgMiner
          /#18842541

          Да, я имел ввиду аннотации, кстати упоминал даже такой подход в статье:

          Чтение PHPDoc и других комментариев

          Изначально я его отбросил, но если, например, упомянутый выше PHPParser умеет читать комментарии, то возможно вернусь к нему, если получится уместить все фичи в синтаксис аннотаций.

          За библиотеку спасибо, посмотрю.