Немного о своём PHP-фреймворке

Где-то с нового года я занимаюсь разработкой своего PHP-фреймворка. Без каких-либо обязательств, просто для себя. Во что это выльется я не знаю, да и цели сделать его публичным пока не стоит. Изначальный посыл был несколько лет назад: я серьёзно подумывал отказаться от CodeIgniter в качестве основы MaxSite CMS. Продумывал новую структуру и всё что с этим связано. Постепенно я понял, что затея тупиковая, поэтому решил сосредоточиться на более простой вещи — микрофреймворке, который даст основу уже полноценной разработке.

За это время фреймворк я переписывал с нуля раз 100. Вначале делал как мне казалось верно, после пытался применить на практике, натыкался на проблему, пытался её решить, понимал, что нужно сделать проще поэтому менял основу и дальше новая итерация цикла.

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

Естественно, я рассматривал десятки самых разных фреймворков, в надежде найти хорошие идеи. В какой-то момент я осознал, что все эти разработки базируются на идеях времён PHP 4. Даже самые современные разработки, которые требуют уже PHP 7.3 по сути принципиально не отличаются от того, что было 10-15 лет назад. Использовать синтаксис новых версий PHP ещё не означает, что внутренняя архитектура проекта выполнена должным образом. Главная проблема даже не в сложности кода, а в запутанности связей между файлами.

Например почти все php-фреймворки предлагают размещать контролёры, модели и представления по раздельным каталогам. В итоге, если мы хотим понять работу модуля, необходимо отслеживать все эти связи. При этом, если мы хотим воспользоваться сторонней разработкой, то она уже оформлена как отдельный модуль, хотя сам фреймворк не является модульным. Абсурд.

Что такое модуль? Любая функциональная часть, что-то вроде плагинов в MaxSite CMS. Когда фреймворк модульный, то он расширяется за счёт модулей, которые всегда располагаются в своём каталоге. То есть не нужно думать, что какой-то файл нужно кинуть в один каталог, а что-то в другой.

Другая проблема — неверное понимание MVC. Контролёр — всего лишь точка связи между моделью и представлением. Когда же контролёр требует конкретную модель или представление, да и сам может быть порожден от какого-то системного класса, то это уже жесткие ограничительные рамки. Ведь что такое MVC на практике? Это связь между классами и не более того. По какой-то неведомой причине, почти во всех фреймворках вводят ограничения, которые заставляют расширять системный класс. То есть вы не сможете написать ни одного модуля на чистом PHP по правилам ООП.

Я рассматриваю фреймворк как набор библиотек с минимальными вкраплениями логики (эдакое Лего). PSR-4 полностью решил проблему с автоподключением классов, но до сих пор используются различного рода «регистраторы», «посредники», «фасады» для указания классов, которые могут использоваться в приложении и т.п. Всё это сильно усложняет алгоритм работы, где невозможно проследить даже очередность прохождения http-запроса.

Как я решил сделать? Первый базовый принцип — следование PSR-4. Это значит, что структура каталогов может быть произвольной. Указываем namespace, имя класс и профит! автозагрузчик сам всё что нужно подключит.

Второй момент — фреймворк не должен указывать как создавать приложение. То есть не существует никаких базовых классов, которые должен наследовать контролёр или модель. Если нужно, то разработчик сам его создаёт.

Третий момент — роутинг — это уровень приложения. Если кратко, то после начальной инициализации, управление передаётся в единую точку входа приложения. Уже в нём создаётся объект роутинга, который и выполнит всё что нужно. Очевидно, что при такой схеме нет никаких сложностей написать роутинг под свою задачу, или использовать любой другой.

Четвертый момент — системные библиотеки необязательны для использования. Не нужны никакие автоподключения или какие-то посредники. Если нужна библиотека просто используем её как это принято в PHP. Никаких лишних сущностей.

Пару примеров.

Обычный «Hello, World!». В конфигурации роутинга указывается адрес и класс:

[
        'pattern' => 'hello', // http://site/hello
        'action' => '\Modules\Demo\Hello\Hello@show', // класс@метод
    ],

Это означает, что для адреса http://site/hello сработает класс «\Modules\Demo\Hello\Hello» с методом «show». При этом сам файл будет: «app/Modules/Demo/Hello/Hello.php».

// Hello.php Controller
 
namespace Modules\Demo\Hello;
 
class Hello
{
    private $model;
    private $view;
 
    public function __construct($routerParam = '')
    {
        $this->model = new Model\Model();
        $this->view = new View\View();
    }
 
    public function show()
    {
        $dataModel['message'] = $this->model->getData();
        $this->view->output($dataModel);
    }
}

Подключаются свои модель и представление. Они находятся здесь же в своих подкаталогах, на что указывает их namespace.

Метод «show» указан в роутере — именно он и сработает. В нём вначале получаются данные от модели, после чего они передаются «вьюшке» для вывода.

Модель просто отдаёт строчку.

// Model.php
 
namespace Modules\Demo\Hello\Model;
 
class Model
{
    public function getData()
    {
        return 'Hello, World!';
    }
}

Представление выводит её в браузер.

// View.php
 
namespace Modules\Demo\Hello\View;
 
class View
{
    public function output($dataModel)
    {
        echo '<h1>' . $dataModel['message'] . '</h1>';
    }
}

Это простой пример, который показывает что код MVC может быть чистым и простым. При этом можно использовать любую композицию или наследование, то есть всё, то что разрешено в PHP.

Когда стоит более сложная задача, например валидация данных формы, то никакой скрытой работы не происходит, всё должно быть явно. Вот пример куска кода модели с подобной проверкой:

use \Lib\Validation\Validation;
... 
 
if ($_POST) {
 
    $rules = [
        'session' => [
            'required' => [
                'error' => 'No session',
            ],
 
            'equals' => [
                'param' => SESSION_ID,
                'error' => 'Error session',
            ],
        ],
 
        'email' => [
            'required' => [
                'error' => 'Требуется указать email',
            ],
  
            'email' => [
                'error' => 'Некорректный email',
            ],
        ],
        ...
    ];
    
    $errors = Validation::validate($rules, $_POST);
    
    if ($errors) {
        $result['errors'] = $errors;
        $result['showForm'] = true;
    }
    else {
        $result['showForm'] = false;
    }
    
    return $result;
    ...

Одна из больших проблем — это работа с шаблонизатором View. Проблема здесь в том, что для корректной работы шаблонизатора требуется указывать каталог размещения «вьюшки». В файле может быть подключение любых других файлов, но технически подключение «вьюшки» выполняется изолировано в отдельной функции, где нет этого каталога. Именно по этой причине во многих фреймворках каталог для View жестко предопределён.

Чтобы обеспечить парсинг файла через шаблонизатор, потребуется не только указывать полный путь к файлу, но и передать во «вьюшку» базовый каталог (если в ней есть ещё подключения).

...
    private $layoutDir = __DIR__ . '/Layout/';
    
    public function showFormPost($dataModel)
    {
        $dataModel['layoutDir'] = $this->layoutDir;
        echo Tmpl::parseFile($this->layoutDir . 'main.php', $dataModel);
    }

То есть в предствлении будет переменная «$layoutDir».

<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title>Form demo</title>
   <link rel="stylesheet" href="<?= RESOURCES_URL ?>shared/css/style.css">
</head>
<body>
   <div class="layout-center-wrap">
      <div class="layout-wrap">
         <?php
 
         if ($errors)
            echo '<ul class="mar30-t w500px b-center text-danger"><li>' . implode('<li>', $errors) . '</ul>';
 
         if ($showForm)
            echo \Lib\Parse\Tmpl::parseFile($layoutDir . 'form.php', $data);
         else
            echo \Lib\Parse\Tmpl::parseFile($layoutDir . 'message.php', $data);
 
         ?>
      </div>
   </div>
</body>
</html>

Такой вариант позволяет строить вывод абсолютно произвольным образом, не завязываясь на предопределённые каталоги.

Ещё один момент, с которым я столкнулся — это то, что почти все php-фреймворки не поддерживают вывод через шаблоны сайта. В них почему-то предполагается, что сайт может иметь только один вариант дизайна. Непонимание этого момента создаёт очень грязный код, как это ни странно.

В примере с формой (это демо-пример) HTML-код модуля формируется полностью. Но на самом деле, форма не должна так делать, потому что её часть работы это только сама форма и сообщения об ошибках. А вот где именно она будет размещаться на сайте — зависит уже от дизайна. То есть схема работы View немного изменяется. После того, как получен html-код модуля формы, данные передаются в шаблон сайта.

...
    $data['content'] = Tmpl::parseFile($fnMain, $dataForm);
    
    Core::siteTemplate($data);
...

Поскольку шаблонов может быть несколько, то текущий задаётся в конфигурационном файле. Получив данные ($data), он уже сам решает что и как выводить. Важно здесь то, что модуль может работать как самостоятельно, либо через шаблон сайта. Никаких скрытых действий.

Отдельно хочу затронуть вопрос о public-части приложения. Существует мнение, что для web-каталога сервера не следует выкладывать основные программные модули — только то, что должен грузить браузер (js, css и т.п.). Я не считаю, что это правильно, но технически нет ограничений задать любые каталоги на сервере. Для этого во фронт-контроллере (index.php) задаются константы для всех базовых каталогов.

...
if (!defined('APP_DIR')) define('APP_DIR', BASE_DIR . $dirs['APP'] . DIRECTORY_SEPARATOR);
if (!defined('SYS_DIR')) define('SYS_DIR', BASE_DIR . $dirs['SYSTEM'] . DIRECTORY_SEPARATOR);
if (!defined('RESOURCES_DIR')) define('RESOURCES_DIR', BASE_DIR . $dirs['RESOURCES'] . DIRECTORY_SEPARATOR);
if (!defined('WRITABLE_DIR')) define('WRITABLE_DIR', BASE_DIR . $dirs['WRITABLE'] . DIRECTORY_SEPARATOR);
if (!defined('COMPOSER_DIR')) define('COMPOSER_DIR', BASE_DIR . $dirs['COMPOSER'] . DIRECTORY_SEPARATOR);
...

Достаточно их переопределить под свою задачу.

Когда стал вопрос, а какой же каталог выделить под web-public, то я решил, что это будет «resources», а каталоги «app», «system» вообще закрыты для просмотра («Deny from all»). Такое решение создаёт некоторые особенности при выводе «полных вьюшек» — для них требуется размещать web-файлы в «resources». Но поскольку это URL-адрес, то используется константа «RESOURCES_URL» (см. пример использования выше) — это полностью решило вопрос как формировать правильные адреса при выводе.

Ну и стоит отметить, то шаблоны сайтов также размещаются в «resources», что упрощает их создание и схему работы.

Комментариев: 12 RSS

1Александр Макаров28-08-2019 19:47

Почти всё из описанного реализовано в Yii 1.1 и Yii 2.0. В 3.0, который ещё разрабатывается, есть всё остальное.

2MAX28-08-2019 20:19

Смешно. Такой дурной код как в Yii, ещё поискать нужно...

3ярик10-09-2019 14:07

мог ли ты бы взять fat-free microframework, чуток переделать под себя и выдать под свободной лицензией? видимо, только она мешает ему стать самым популярным или самым лучшим.

5Сергей27-12-2019 18:33

Здравствуйте!

Скажите, почему Вы используете $_POST напрямую?

И почему все утверждают, что ни-ни не надо этого делать.

Я вот хочу сделать самую простую CMS на чистом PHP для нужд самых скромных сайтов.

В index.php я делаю так:

if (count($_GET) > 0) $_GET = secure_clean($_GET);
if (isset($_POST)) $_POST = secure_clean($_POST);

в файле secure.php:

function secure_clean($data) {
  if (is_array($data)) {
    foreach ($data as $key => $value) {
      unset($data[$key]);
      $data[secure_clean($key)] = secure_clean($value);
    }
  } else {
    $data = trim(htmlspecialchars(addslashes($data), ENT_COMPAT, 'UTF-8'));
  }
  return $data;
}

Затем вместо привычного для CMS

$request->post('key');

Использую

$_POST['key']

Мне просто в голову не залезает, с чего вдруг мне хаотично перезаписывать значение $_POST['key'], чего все так боятся...

6MAX27-12-2019 18:48

Ну а методы

$request->post

что по вашему возвращают? Именно $_POST, поскольку так устроен PHP. Можно, конечно через потоки, но результат будет таким же.

Что касается валидации, то она нужна обязательно. У меня свой класс Validation, который проверяет каждое post-поле по заданным правилам. Это давно устоявшаяся практика. В вашем примере валидация бесмысленная, поскольку неясно для чего будет использоваться post-данные. Если это sql-запрос, то валидация должна проходить через PDO prepare. Если это пароль, то через фильтр разрешенных симовлов и т.д. То есть сама по себе обработка $_POST не имеет смысла. Это всего лишь данные и ничего более.

7Сергей28-12-2019 15:40

Спасибо за то, что отвечаете на комментарии.

1. Так я почему озадачен? Потому что я не понимаю, для чего нужно использовать

$request->post

если можно просто $_POST?

Из того, что читал, вообще нигде не объясняется четкая причинно-следственная связь, чем же все таки так плохо обращаться к $_POST. А все предупреждают, что нельзя. Даже IDE пишет предупреждение, что обращаться к $_POST плохо. И я никак не пойму, чего я не понимаю. Подумал, что Вы-то точно знаете чуточку больше, чем я. Я просто прошел пару курсов, образования в сфере информатики нету.

Насколько я понял Вас, то можно обращаться к $_POST, если не тупо следовать "моде"? А валидация - зависит от контекста и не должна осуществляться на всякий случай.

2. На какой стадии находится Ваш фреймворк? :)

3. Немного неудобно у Вас в комментариях то, что если ввел ошибочный код, то все сообщение теряется. Но это так, заметки на полях.

8MAX28-12-2019 16:30

Фреймворк — это вещь только для себя. :-)

Обращение к $_POST не создает проблем — это суперглобальная переменная, автоматически создаваемая PHP (как часть обработки HTTP метода post). В языке не существует никаких ограничений при прямом обращении к $_POST. Даже если используются другие функции, то они являются оберткой над этой переменной. Так что если кто-то декларирует ограничения, то только в рамках своих проектов, чтобы заставить других пользоваться единообразным функционалом. Но, с точки зрения PHP, не существует абсолютно никаких ограничений для $_POST — это неотъемлемая часть языка и все ограничения существуют только в головах программистов. :-)

9Аноним05-05-2020 14:27

Насколько я помню, MaxSite CMS тоже создавался для себя.

10Аноним02-10-2020 07:28

А какой фреймворк посоветуете? Учитывая то, что вы о них пишете в своем блоге.

11MAX02-10-2020 09:03

Это зависит от задачи. Из простых и понятных — CodeIgniter. Остальные смотрите по тому, какой там доступный функционал. Ну и дополнительные критерии — сложность осваивания, поддержки, обновления, нагрузки на сервер, требования к серверу и т.д.

12Старый прохожий13-10-2020 21:07

Может стоит задуматься заодно и о смене языка? Строить что-то на платформе тонущего корабля как-то...

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)