Яким може бути фреймворк-незалежний код на PHP

Я, Романенко Сергій, розробник веб-застосунків мовою програмування PHP. Наразі я маю декілька розробок:

  • міні-фреймворк php-server,
  • DI-контейнер php-container на базі PSR-11,
  • робочий приклад сайту, який розроблений на базі вищевказаних інструментів і код якого є відкритим на GitHub,
  • JavaScript компоненти, які використовую на фронтенді.

Мій досвід розробки на чистому PHP зараз приблизно 2 роки.

Чому ця стаття

До написання даної статті мене підштовхнула рецензія на мою роботу:

«У вашому застосунку немає валідації»

Фреймворки — це сучасний і зручний засіб швидкої розробки проєктів. Без них, створення сайтів багатьом здається неможливим. На жаль, зчеплення більшості коду застосунків з обраним фреймворком таке, що їх вже неможливо або дуже складно (дорого) відокремити один від одного. Мало того, що застосунок «прибитий» надійно до конкретного фреймворку, так він ще і залежить від його конкретної версії. А це вже спричиняє проблеми при оновленні до останньої версії.

У цій статті я хочу показати, як можна писати код, без прив’язки до конкретного фреймворку. Якщо використовувати запропоновану мною практику, то застосунок буде залежити тільки від мови програмування і її версії.

Я розгляну такі сучасні фреймворки як Laravel та Symfony.

Розділення коду між застосунком та фреймворком

Для того, щоб писати фреймворк-незалежний код, треба чітко розуміти, що можна віднести до коду застосунку, а що до коду фреймворку.

Код застосунку — це код, який можна скопіювати в середовище іншого фреймворку без внесення жодних змін.

Код фреймворку — це код, який залежить від конкретного фреймворку або від його поточної версії. Такий код неможливо перенести в інший фреймворк без його модифікації або повної переробки.

Код застосунку

Код застосунку — це основна кодова база, ядро проекту. Особливості такого коду:

  • знає лише про себе,
  • зав’язаний на інтерфейси,
  • виконує бізнес-логіку.

У своїх проєктах я використовую два шари коду застосунку:

  • Domain,
  • Application.

Domain — це сутності застосунку. Код знає тільки про себе. Він навіть не бачить Application. В цьому блоці зібрані найголовніші правила, логіка, валідація. Цей код диктує умови всьому застосунку. Він може отримувати дані з Application і залежати тільки від своїх інтерфейсів.

Application — це сервіси застосунку, які мають доступ до Domain і знають як правильно з ним працювати. Саме тут створюються, змінюються сутності. Також на цьому рівні виконується керування логікою. Application працює з інфраструктурою на основі своїх інтерфейсів, наприклад базою даних, логуванням тощо.

У цих двох шарах не повинно бути жодного зчеплення з фреймворком, в тому числі з його інтерфейсами. Якщо це так, то ваш код є незалежним від фреймворку.

Код фреймворку

Код фреймворку — це код, який зв’язує поточну інфраструктуру з Application через інтерфейси.

У своїх проєктах я використовую тільки один шар коду фреймворку:

  • Infrastructure.

Всередині він розділяється по напрямках:

  • Http (контролери і темплейти),
  • CLI (команди),
  • Persist (бази даних, сесія, файли),
  • Notification (емейл, месенджери),
  • тощо.

Саме цей код є залежним від конкретного фреймворку і може навіть залежати від його конкретної версії. Все, що знаходиться в цьому розділі, має бути проаналізовано і перероблено під час оновлення до нової версії або перенесенні на інший фреймворк.

Шар Infrastructure працює з Application в дві сторони:

  • Infrastructure викликає Application без інтерфейсів;
  • Application викликає Infrastructure через свої інтерфейси.

Приклади залежності коду застосунку від фреймворку

Враховуючи, що розробники фреймворків самі пропонують високе зчеплення коду застосунку з їхнім кодом, нескладно здогадатися, що це стало поширеним явищем.

Далі я розгляну приклади зчеплення коду з Laravel та Symfony. Я впевнений, що інші фреймворки крокують у тому ж напрямку.

Laravel

Пропоную розглянути зчеплення на прикладі валідації даних. На цій сторінці є такий код:

/**
 * Store a new blog post.
 */
public function store(Request $request): RedirectResponse
{
    $validated = $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ]);

    // The blog post is valid...

    return redirect('/posts');
}

Розробнику пропонується в контролері проводити валідацію даних доменної частини.

Чому це невірно:

  • порушення принципу єдиної відповідальності — ці дані належать сутності, яка ними володіє, а не контролеру;
  • дублювання коду — вам доводиться валідувати дані в контролері, команді cli, події, в моделі з бази даних тощо, що може призвести до помилок.

Таких прикладів ціле море на сайті Laravel, тож немає сенсу обговорювати всі.

Symfony

В Symfony та сама проблема з валідацією даних, що і в Laravel і ще багато інших. Пропоную розглянути приклад моделі сутності зі сторінки.

// src/Entity/Product.php
namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column]
    private ?int $price = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    // ... getter and setter methods
}

Це приклад сутності продукту на сайті. Розробнику пропонується обтяжити модель атрибутами для валідації вхідних даних.

Чому це невірно:

  • код доменної частини прив’язується до фреймворку (ORM) — такий код не буде працювати як очікується, якщо його перенести в інше середовище.
  • метод __construct не може правильно ініціалізувати сутність — ви не можете привести сутність у відповідний стан і як наслідок вона переходить у неконтрольований стан.
  • порушення принципу єдиної відповідальності: вхідні дані належать сутності, яка ними володіє, а не ORM;

За аналогією з Laravel, немає сенсу розглядати усі приклади з сайту Symfony так як їх багато.

Готові приклади незалежного від фреймворку коду

Спеціально для вас я підготував два окремих репозиторії одного й того ж API:

Код застосунку — один і той самий, знаходиться в каталогах Application і Domain.

Код фреймворку — різний. Це контролер та репозиторій.

В каталозі docs знаходиться завдання, згідно якого побудовано дане API. У двох словах — підрахунок вартості доставки перевізника залежно від ваги посилки. Читати всього 2 хвилини.

Процес встановлення дуже простий, перевірений мною, тож проблем не повинно виникнути.

Практично все покрито тестами. Майже все. Це не комерційний продукт, тому я не ставив собі завдання протестувати по максимуму. Суть не в тестах.

Я не буду тут описувати як працює мій код, тому що це зайняло б багато місця. Спеціально для цього я підготував окремий файл в репозиторії Laravel. У випадку Symfony, відмінність тільки в репозиторії, який зв’язаний із конфігурацією, а не базою даних. Читати всього 5 хвилин.

Статистика

Підсумовую кількість файлів по шарам. Фреймворк-незалежних файлів усього — 17. З них Application — 11, Domain — 6. Фреймворко-залежних файлів — 2. З них контролер — 1, репозиторій — 1.

ШарКулькістьзалежність
Domain6незалежний
Application11незалежний
Http1залежний
Persist1залежний

Як бачите, результат 17 проти 2. Іншими словами при оновленні або перенесенні на інший фреймворк, вам необхідно буде проінспектувати/змінити тільки 2 файли!

* Є ще декілька файлів в каталозі Controller, по яких я не можу із впевненістю визначити залежність. Йдеться про DTO, які формують кінцеву відповідь для користувача. З одного боку вони незалежні від фреймворка, з іншого — це інфраструктура.

Відмінності

Даю порівняльну таблицю, щоб було зрозуміліше, чим приклади відрізняються один від одного.

ІнфраструктураРеалізація
Http/ControllerLaravelSymfony
Persist/Repositorymysqlconfig
View/Templatebladetwig

Висновок

В якості висновку хочу зазначити як позитивні, так і можливі негативні сторони застосування запропонованої мною техніки.

Спочатку про позитивне:

  • на кожен блок (точку) застосунку мінімум змін у майбутньому (17 проти 2 як у прикладах вище);
  • єдина точка валідації даних;
  • повний контроль над станом кожної сутності.

Щодо можливих негативних наслідків:

  • треба перевчатися;
  • ORM відкидається, DBAL використовується.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному1
LinkedIn
Ctrl + Enter
Ctrl + Enter

А я вам скажу, що треба викинути пхп. Є .net core, і там вже немає сенсу думати про фреймворк незалежний код. Під час користування, я побачив неймовірну схожість з symphony та laravel. Тому не треба оце голову забивати отими фреймворками, забагато зайвого. Одна мова, один фреймворк, .net і досить

У статті я посилаюся не лише на залежність від фреймворку, а й від його версії. Впевнений, що в .NET теж є версії, і, швидше за все, деякі з них між собою несумісні.
Тож ви можете використовувати запропоновану мною практику для швидшого й упевненішого оновлення до нової версії .NET.

Спостерігаю досить дивний перекос в затребуваності PHP розробників залежно від континенту.
Якщо на території Євразії повно об’яв і ці спеціалісти потрібні. То наприклад в Північній Америці наприклад в Канаді складається враження що це вимираючий напрямок.

Дивлюсь вакансії на LinkedIn, Indeed (привіт Djinni) та інших ресурсах (з регіональним фільтром) і бачу що там вакансій кіт наплакав. Чистий бек нікому не цікавий, але навіть так тут про Symfony взагалі ніхто не чув (дуже стійке враження) тільки Laravel + NodeJS|NestJs|React і понеслася.

Окрема каста це Wordpress розробники зі 100500 роками досвіду тут це прям окрема ніша.
Всі інші вакансії це С#|Python|NodeJs зі знанням PHP бо потрібно переписувати правити переносити функціонал зі старого проекту.
Кожна друга та ні 90% вакансій це вимога fullstack.
Мав спілкування з місцевими і мені пояснили що ПоХаПе тут давно померло ))

Так я останні років двадцять спотстерігаю різні срачі шо пих помре але тут я прям бачу як на ньому вже трава поросла.

Єдине що незрозуміло це чисто регіональний розподіл чи до Европи це теж докотиться.
Вибачайте за сумбурність потоку сознанія, але хочу про це поговорити :-)

Коментар порушує правила спільноти і видалений модераторами.

Я так не зміг зрозуміти, яка практична користь від подібного розділення коду? Закласти базу для теореточної заміни фреймворку в майбутньому?
Я стикався з переходом проєктів на повністю іншу платформу, наприклад з .NET на Java, або з PHP на NodeJS. Міграція на інший фреймворк для тієї ж самої мови мені здається малоймовірною.

Якщо ви розробляєте продукт, який буде працювати довго, є сенс заздалегідь передбачити можливість переходу на інший фреймворк або на його нову версію. Звісно, ніхто не змушує вас так робити. Клієнт узагалі може про це не знати.

Які можуть бути причини для переходу? Що завгодно. Наприклад, у вас є продукт, який приносить багато прибутку і побудований на фреймворку, що перестав оновлюватися або майже не розвивається. Такого не може бути?

Наприклад, Yii 2 і Yii 3 між собою несумісні.

це все прикольно, але маю комерційний проект на yii1 ( де в ядрі фреймворка навіть неймспейсів не було, і DI)
там тисяч 20+ класів

так от, не уявляю скільки треба часу щоб то перепроектувати і перелізти на інший фреймфорк + нові фреймворки не гарантують performance. yii1 був найлегшим і найшвидшим, symfony / laravel за рахунок кешів намагаються компенсувати навалу абстракцій, але навіть стектрейси в yii1 і laravel/symfony з їх DI просто непорівнювані.

ідея в цілому прикольна, але наш шлях — це переписувати модулі легасі проекта на go + десь переглянути логіку, бо не завжди існуючий код ідеально вірний і часи вносять необхідність змінювати як раз domain левел

тому двояке відчуття, глянув статтю, бо думав прям дійсно щось круте буде, і... чесно ідея з шарами мені зайшла!,
але практично застосувати це нереально, якщо не починаєш новий проект...
а якщо і починаєш — то в «завтрашній день можуть дивитися не тільки лиш всі», щоб знати що твій фреймфорк перестане бути модним і підтримуваним

Якщо у вас Yii1, то це php 5 і звісно запропонований мною підхід вам не підійде. Це треба оновлювати версію мови на більш новішу. Інакше я навіть не знаю як.

На Yii2 складно писати фреймворк незалежний код, а якщо так робити то від фреймворка там майже нічого не залишиться, краще вже взяти наприклад slim

Підписатись на коментарі