Bricks: конструктор для новичков

1.28K
.
Простейший фреймворк
Переходим к одному из самых интересных разделов - сборке фреймворка. Как я уже отмечал ранее, фреймворк определяет будущую структуру приложения, но благодаря использованию готовых пакетов, вы можете самостоятельно сделать это так, как вам будет удобно.

Пока мы реализуем простейшее решение, позволяющее только формировать Web-страницы и отдавать их в качестве ответов сервера. Фреймворк будет лишен модульной архитектуры, следовательно будет монолитным приложением, расширение которого представляется добавлением новых компонентов в имеющиеся каталоги. Так же во фреймворке не будет множества полезных и удобных программных решений, о которых мы поговорим позже.

Для начала определимся с файловой структурой приложения. Я считаю удобной следующее решение:

app/
  conf/
    global.conf.php
    local.conf.php
  controller/
  model/
  App.php
  composer.json
  layout.html
public/
  script/
  style/
  index.php


Разберем некоторые основные компоненты фреймворка. В корневом каталоге приложения помимо каталогов app и public так же могут размещаться проектные файлы, на пример каталог test/ с модульными тестами, файлы README.md, .gitignore, migration.sh и т.д. на ваше усмотрение. В каталоге public/ размещаются общедоступные файлы проекта, такие как JS скрипты, CSS файлы и точка входа - файл index.php. Каталог app/ хранит логику самого приложения, и его следует рассмотреть подробнее.
.
Каталог app
Данный каталог состоит из следующих компонентов:
conf/ - файлы конфигураций приложения. Как правило это два файла: global.conf.php - для общих конфигураций; local.conf.php - для локальных конфигураций. Это необходимо для работы в команде, так как отдельному разработчику может потребоваться сконфигурировать, на пример, подключение к БД отличным от других разработчиков образом
model/ - классы бизнес-логики приложения
controller/ - контроллеры приложения
composer.json - зависимости приложения. Использование Composer так же добавит каталог vendor/
layout.html - основной шаблон HTML страницы
App.php - класс инициализации приложения

Файл layout.html представляет собой обычный файл с HTML разметкой, содержимое которого не изменяется. Он используется как шаблон при генерации ответа сервера и содержит следующую разметку:

<!DOCTYPE html>
<html>
<head>
<title>Microframework</title>
<meta charset="utf-8" />
</head>
<body>
<?= $content ?>
</body>
</html>


Как видно, результаты обработки запроса могут быть вставлены в тело HTML-документа с помощью переменной $content, но об этом чуть позже.
.
Файл index.php
Всех запросы к приложению должны поступать на файл public/index.php. Это упростит их обработку. Сделать это можно настроив соответствующим образом Web-сервер, но это не тема нашего обсуждения. Я лишь приведу пример содержимого индексного файла:

<?php
chdir(dirname(__DIR__)); // Определяем корневой каталог приложения.

include('app/App.php'); // Подключаем класс инициализации приложения
(new App)->run();       // и запускаем его


Описывать здесь нечего, в файле только подключается и вызывается класс инициализации приложения, не более того.
.
Delphinum
Инициализация приложения
Вся логика фреймворка, его архитектурные ограничения и возможности мы опишем в файле app/App.php. Этот файл очень важен, так как именно он отвечает за инициализацию всего приложения и доступ к его частям, но чтобы вам было проще понять как он выполняет свои функции, рассмотрим его составные части последовательно.

Для начала рассмотрим структуру класса инициализации:
<?php
// Файл app/App.php
require('vendor/autoload.php');
use Bricks\Autoload\Loader;
use Bricks\ServiceLocator\Manager;
use Bricks\Http\Routing\Router;
use Bricks\Http\Routing\Request;
use Bricks\Http\Routing\Response;

class App{
  public function run(){
    ...
  }
}


В классе подключается автозагрузчик зависимостей, создаваемый Composer, а затем объявляются все используемые в нем классы пакетов Bricks. Структура самого класса App довольно проста. Он не относится к какому либо пространству имен (namespace), так как это единственный модуль в приложении. В нем реализуется единственный метод run, который и вызывается из файла public/index.php и инициализирует приложение в целом.

Рассмотрим реализацию метода run. В первую очередь в нем необходимо подключить и настроить автозагрузчик, так как он используется всеми компонентами приложения:
public function run(){
  $loader = new Loader;
  $loader->pref('conf', 'app/conf');   // Конфигурации ищем в app/conf
  $loader->pref('model', 'app/model'); // Бизнес-логику в app/model
  $loader->pref('controller', 'app/controller'); // а контроллеры в app/controller
  ...
}

Думаю здесь комментарии излишни.

Далее необходимо заполнить инициализировать локатор служб. Этот механизм позволит всем частям приложения получить доступ к доступным компонентам фреймворка:
public function run(){
  // Автозагрузчик

  $locator = new Manager;

  $router = new Router; // Создадим роутер предварительно, чтобы его можно было добавить в локатор служб.

  $locator->set('loader', $loader); // В локатор добавляется автозагрузчик
  $locator->set('router', $router); // роутер
  $locator->set('conf', array_merge($loader->load('conf\global.conf'), $loader->load('conf\local.conf'))); // и конфигурации приложения.
  ...
}


Теперь самое интересное - настройка роутинга:
public function run(){
  // Автозагрузчик
  // Локатор служб

  // Обработка запросов вида http://site.com/controller/action
  $router->all('~^/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)~',
    function(Request $request, Response $response, array $match) use($locator){
      list($subject, $action) = $match; // Определяем целевой контроллер и метод.
      $controller = 'controller\\' . ucfirst($subject); // Так как мы определили в автозагрузчике местонахождение всех контроллеров, его будет легко подключить.
      $controller = new $controller; // Создаем экземпляр контроллера
      $method = $action . 'Action';  // и определяем имя целевого метода как "имяAction"

      return $controller->$method($request, $response, $match); // Вызываем метод контроллера возвращая результат его работы.
    }
  );
  ...
}

Этот маршрут подходит для запросов на адрес вида: http://site.com/controller/action - но этого недостаточно, необходимо так же определить маршрут для запросов на адрес без URI: http://site.com - что мы и сделаем:
public function run(){
  // Автозагрузчик
  // Локатор служб
  // Маршрут A

  // Все запросы без URI передаем в контроллер controller\Index::indexAction
  $router->all('~^/?$~',
    function(Request $request, Response $response, array $match) use($locator){
      $controller = new controller\Index;
      return $controller->indexAction($request, $response, $match);
    }
  );
  ...
}


Осталось только выполнить маршрутизацию:
public function run(){
  // Автозагрузчик
  // Локатор служб
  // Маршрут A
  // Маршрут B

  $request = new Request;
  $response = new Response;
  $response->header('Content-Type', 'text/html;charset=utf-8'); // Определяем тип ответа как HTML-документ в кодировке UTF-8.
  $content = $router->run($request, $response); // Выполняем маршрутизацию и получаем ответ контроллера.
  include($loader->path('app/layout', 'html')); // Загружаем используемый нами шаблон страницы. Так как ответ контроллера записан в переменную $content, он будет вставлен в тело шаблона автоматически.
  $response->send(); // Отправляем ответ клиенту.
}


Полный листинг класса App приведен здесь:
App (+/-)


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

Чаще всего Web-приложения оперируют с пакетами, содержащими несколько именованных данных, на пример такими:

$_POST = [
  'id' => '123',
  'message' => 'Тестовое сообщение в чате',
  'author_id' => '1',
];


Обрабатываться данные в пакете должны в соответствии с некоторой "картой валидации". К примеру идентификатор (id) должен быть сначала приведен к целочисленному типу, а затем проверен на вхождение в диапазон (значение должно быть больше нуля). Это можно сделать так:

$id = (int) $_POST['id'];
if($id <= 0){
  die('Недопустимые данные');
}


Этого достаточно для обеспечения безопасности системы, но проверять все данные из всех пакетов становится довольно накладно. Более того, обычно для проверки применяются одинаковые механизмы, но в разной последовательности. Этот набор механизмов проверки и называется "картой валидации".

Пакет Bricks.Validation облегчает и унифицирует процесс проверки пакетов данных сводя его к определению "карты валидации" для конкретного пакета.

Прежде чем начать использовать этот пакет, необходимо научиться различать "Валидатор" и "Фильтр":

Фильтр - механизм изменяющий данные и приводящий их к некоторому виду (на пример приведение к целому типу или trim по строке). Фильтр не проверяет данные, а только подготавливает их
Валидатор - механизм проверяющий данные на предмет нахождения в них недопустимых значений (на пример отрицательных чисел или слишком длинных строк)

Предложенный мной пакет реализует несколько готовых классов фильтрации и валидации данных, позволяющих обрабатывать большинство пакетов данных, используемых в Web-приложениях. Рассмотрим некоторые из них:

Фильтры
Int, String - приведение к целочисленному значению или к строке
Trim - обрезка строки с начала и конца
IntRange, StringRange - приведение числа или длины строки к указанному диапазону
Path - выделение компонента из адреса файлового ресурса (на пример имени файла или расширения)
Url - выделение компонента из URL адреса (на пример порта или хоста)
Валидаторы
NoEmpty - не пустые данные
Length - проверка длины данных
Regex - проверка строки с помощью регулярного выражения
Email - проверка email на корректность
Ip - проверка IP адреса на корректность
Url - проверка Url на корректность

Вы можете создавать свои фильтры и валидаторы и использовать их для пакетной валидации данных в своих приложениях.

Пакетная валидация выполняется с помощью класса Input данного пакета. При инстанциации этого класса конструктору необходимо передать "карту валидации", которая будет применяться объектом к проверяемым данным. Карта представляется в виде ассоциативного массива, фильтры в котором указываются с ведущим знаком !, а валидаторы со знаком ?. Порядок перечисления очень важен, так как именно в этом порядке они будут применяться к данным.

Пример карты валидации:

use Bricks\Validation\Input;

$input = new Input([
  'id' => ['!int', '?length', 'min' => 1],
  'message' => ['!string', '?length', 'max' => 256],
  'author_id' => ['!int', '?length', 'min' => 1],
]);


Разберем этот пример. Данная "карта валидации" определяет следующие правила:
Для id - сначала приведение данных к целочисленному типу с помощью фильтра Int, затем валидация полученных данных с помощью валидатора Length дабы число было больше или равно 1
Для message - сначала приведение данных к строковому типу с помощью фильтра String, затем валидация полученных данных с помощью валидатора Lenght, дабы длина строки не превышала 256 символов
author_id обрабатывается аналогично id

Когда пакетный обработчик создан, с помощью метода validate ему можно передать массив данных, которые он проверит и вернет подготовленный (безопасный) пакет или выбросит исключение, если данные не безопасны:

...
$data = $input->validate($_POST);


Довольно просто, не правда ли? Суть этого метода заключается в применении таких "карт валидации", которые позволят максимально гибко и безопасно обработать данные. Рассмотрим еще один пример более сложной валидации:

use Bricks\Validation\Input;

$input = new Input([
  'message' => ['!string', '!trim', '?length', 'min' => 1, 'max' => 256],
  'from' => ['!string', '!trim', '?email'],
  'file' => ['!string', '!trim', '!path', 'path' => 'basename', '?length', 'min' => 1],
]);


Архив с примером.
.
Delphinum
Инъекция зависимостей
Инъекция зависимостей или DI, довольно спорный, но очень удобный механизм. Здесь я опишу лишь одну из реализаций DI, но встречаются и другие. В общем смысле DI это такой механизм, который позволяет по имени члена класса или аргумента функции получить конкретный сервис.

Рассмотрим несложный пример. Предположим у нас есть контроллер, в котором реализуется пара методов:
<?php
class NewsController{
  public function getAction(){
    ...
    $user = User::getCurrentUser();
    $userId = $user->getId();
    ...
  }

  public function saveAction(){
    ...
    $log = ServiceLocator::get('log');
    $log->info(...);
    ...
  }
}

Не имеет значение что именно делают эти методы, важно лишь то, что в них мы напрямую обращаемся к некоторым службам, а именно получает экземпляр текущего пользователя и службу логгирования. Чем плоха такая реализация? Ее довольно тяжело тестировать, так как нам придется не просто подменить используемые службы специальными объектами-заглушками, но и как то передать их в тестируемые методы. Решить эту проблему довольно просто:
<?php
class NewsController{
  private $currentUser;
  private $log;

  public function __construct($currentUser, $log){
$this->currentUser = $currentUser;
$this->log = $log;
  }

  public function getAction(){
    ...
    $userId = $this->currentUser->getId();
    ...
  }

  public function saveAction(){
    ...
    $this->log->info(...);
    ...
  }
}

Как видно, достаточно просто вынести логику доступа к зависимостям за пределы класса. Другими словами классу не важно откуда берутся его зависимости, они лишь устанавливаются ему через конструктор и им же используются. Тестировать такой класс становится намного проще, но вот инстанциировать, сложнее. Теперь, чтобы получить экземпляр этого класса не достаточно просто использовать операцию new, нужно так же разрешить все зависимости контроллера. Как раз эту задачу решает DI.

Пакет Bricks.Di реализует два механизма разрешения зависимостей:
1. Зависимости контроллера - это те зависимости, которые рассмотрены в примере выше
2. Зависимости вызываемого метода - это любые зависимости, представленные в виде имен аргументов вызываемого метода
Как правило, этого вполне достаточно для большинства задач, для которых применяется DI.

Данный пакет представлен классом Bricks\Di\Manager, который реализован в виде декоратора над локатором служб, массивом, или любым классом, реализующим интерфейс ArrayAccess стандартной библиотеки PHP. Локатор используется DI для получения целевых зависимостей, а сам менеджер реализует механизм выявления этих зависимостей в методах класса. Работает он достаточно просто, потому одного примера должно быть достаточно:
use Bricks\ServiceLocator\Manager as ServiceLocator;
use Bricks\Di\Manager as Di;

// Подготовка локатора служб.
$locator = new ServiceLocator;
$locator->set('currentUser', User::getCurrentUser());
$locator->set('log', new Log);

// Оборачивание локатора в инъектор зависимостей.
$di = new Di($locator);

// Разрешение зависимостей конструктора контроллера и получение его экземпляра.
$controller = $di->constructInjection('NewsController');

Все довольно просто, не правда ли? Сначала формируется локатор служб, затем он оборачивается в DI, а после вызывается конструктор контроллера через механизм DI, который разрешает все его зависимости (на основании имен аргументов конструктора). На выходе мы получаем экземпрял контроллера с разрешенными зависимостями. Это похоже на узкоспециализированную фабрику объектов.

Как уже было сказано ранее, пакет так же позволяет разрешать зависимости вызываемых методов по аналогии с конструктором. Делается это аналогично просто:
use Bricks\ServiceLocator\Manager as ServiceLocator;
use Bricks\Di\Manager as Di;

class NewsController{
  ...

  public function deleteAction($cache){
    ...
    $cache->remove(...);
  }
}

$locator = new ServiceLocator;
...
$locator->set('cache', new Cache);

$di = new Di($locator);

$controller = $di->constructInjection('NewsController');
$result = $di->methodInjection($controller, 'deleteAction');

Здесь выполняется та же самая операция инъекции зависимостей, но не для создания экземпляра класса, а для вызова метода с разрешением его зависимостей.

Архив с примером.
.
reaper
Delphinum,
В общем смысле DI это такой механизм, который позволяет по имени члена класса или аргумента функции получить конкретный сервис.


Ну нет. Это уже детали реализации. Правильнее было бы сказать, что внедрение зависимостей -- это форма инверсии управления (Inversion Of Control, IOC).
В документации к PHP-DI очень хорошо описан этот паттерн:

Как приблизительно работает код, не использующий DI:

Application нуждается в Foo, поэтому:
Application создаёт Foo
Application вызывает Foo
Foo нуждается в Bar (e.g. a service), поэтому:
Foo создаёт Bar
Foo вызывает Bar
Bar нуждается в Bim, поэтому:
Bar создаёт Bim
Bar делает что-либо

Используя внедрение зависимостей:

Application нуждается в Foo, который нуждается в Bar, который нуждается в Bim, поэтому:
Application создаёт Bim
Application создаёт Bar и передаёт ему Bim
Application создаёт Foo и передаёт ему Bar
Application вызывает Foo
Foo вызывает Bar
Bar делает что-либо

Это и есть инверсия управления. Контроль зависимостей инвертирован от вызванного к вызывающему.
.
reaper, не хотел в даваться в такие подробности, но да, это верное замечание
.
Ей 25
Добрался я наконец до изучения этой темы. Пока что осилил несколько постов. Однозначно автор молодец!
Есть уже и вопрос к автору: Брикс это твой фреймворк(набор пакетов)?
.
ValekS, Мой набор пакетов. Фреймворк вы пишете сами.
Всего: 49