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

1.27K
.
Delphinum
Содержание
Введение
Термины и определения
Скучная, но важная теория
Установка и использование пакетов Bricks
Самостоятельное изучение пакетов Bricks
Автозагрузчик ресурсов
Локатор сервисов
Роутер
Простейший фреймворк
Каталог app
Файл index.php
Инициализация приложения
Валидатор
Инъекция зависимостей
Сессии
Шаблонизатор
Менеджер модулей
Модульный фреймворк
ORM
Лог

Пакеты Bricks
Автозагрузчик ресурсов приложения
Локатор служб, предоставляющий глобальный доступ к компонентам приложения
Маршрутизатор HTTP-запросов
Маршрутизатор CLI-вызовов
PHP-шаблонизатор
Менеджер модулей, реализующий логику загрузки и инициализации модульного приложения
Реализация событийной модели взаимодействия
Валидатор данных
Инъекция зависимостей
Провайдер сессий
Шаблонизатор
Менеджер модулей
ORM
Лог
Аннотации
.
Вводная
Этот цикл статей преследует цель не прорекламировать вам мое решение, а объяснить основы современных архитектурных решений в области крупных фреймворков. Здесь я постараюсь реализовать полноценный фреймворк у вас на глазах путем смешения и конфигурации различных пакетов. В результате вы познакомитесь с внутренностями многих современных фреймворков (на концептуальном уровне), это позволит легко изучить любой из них.

Сразу хотелось бы определить целевую аудиторию, так как начинать с основ PHP я не буду. Для понимания тем, которые я затрону вам будет необходимо знать и уметь следующее:
PHP на хорошем уровне, в том числе стандартную библиотеку функций и классов этого языка
Git и GitHub, так как используемые мной пакеты будут доступны в виде нескольких репозиториев
Composer, так как собирать проект мы будем на базе этого пакетного менеджера
PDO и базы данных (используем MySQL) в целом

В процессе написания этого цикла многие компоненты могут изменится, так как проект находится в активной разработке. Я стараюсь использовать максимально простые и общие решения, что уменьшает необходимость в частых изменениях функционала, но все же вы должны быть к этому готовы.
.
Термины и определения
Дабы говорить с вами на одном языке, определимся с терминологией.
Пакет - пакетом я называю готовое решение некоторой задачи, представленное в виде PHP скрипта (классов, данных, конфигураций, тестов, документации), имеющего четкую область применения. К примеру возьмем пакет для решения задачи шаблонизации. В этот пакет может входить один класс, который будет загружать требуемый HTML файл и рендерить его, а может быть и целая библиотека типа Smarty. Оба решения я называю пакетами, так как в них четко определена задача и они являются не более чем частью чего либо большего
Библиотека - множество пакетов, объединенных общей идеей. На пример это может быть библиотека для работы с БД, в которую входит как пакет PDO, так и реализации ORM шаблонов
Фреймворк - архитектурное решение, которое определяет структуру приложений, написанных на его основе. Во фреймворке важно именно то, что он определяет структуры программы. Написать программу можно и без использования готовых фреймворков, но когда вы определитесь со структурой вашей программы, вы по сути изобретете собственный фреймворк
CMS - готовое приложение, которое позволяет управлять контентом. Внутри CMS может использоваться некоторый фреймворк, но в отличии от онного, CMS это готовое решение с админкой, таблицами в БД, системой авторизации и т.д. Пользователю остается только создать контент

Чтобы было понятнее, можно изобразить зависимости этих частей приложения в виде следующей схемы:
CMS(Фреймворк(Библиотека(Пакеты)))

Здесь мы рассмотрим некоторые готовые пакеты, а так же реализуем фреймворк на их основе.
.
Скучная, но важная теория
Буквально все web-фреймворки на любом языке программирования (но мы о PHP) имеют схожее содержание. Чаще всего они состоят из следующих пакетов:
Автозагрузчик - механизм, позволяющий загружать ресурсы приложения, такие как классы, файлы конфигураций или файлы шаблонов
Локатор служб - глобальное хранилище компонентов приложения, таких как конфигурация, системы доступа к БД, используемый шаблонизатор, автозагрузчик и т.д.
Шаблонизатор - для генерации результирующих HTML страниц используется шаблонизация
Роутер - механизм позволяющий маршрутизировать HTTP запросы к обработчикам

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

Процесс взаимодействия клиента с сервером через конкретный фреймворк можно представить в виде следующей схемы:
Клиент -> Сервер -> Роутер -> Модуль -> Контроллер -> Бизнес-логика -> Шаблонизатор -> Клиент
Это классическая MVC схема, наложенная на Web-сервер. Все запросы приходят в контроллер, который передает их в модель (бизнес-логика), которая в свою очередь оповещает представление (шаблонизатор).
.
Delphinum
Установка и использование пакетов Bricks
Как я уже говорил ранее, Bricks это набор простых и легковесных пакетов, которые доступны из репозитория на GitHub. Список доступных на данный момент пакетов можно посмотреть в первом посте темы.

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

Любой пакет Bricks можно установить двумя способами:
Ручная установка
Самым простым вариантом является загрузка архива пакета из репозитория, его распаковка в проект и подключение к с помощью инструкции include/require или используемого механизма автозагрузки.

Приведу пошаговый пример установки пакета ServiceLocator. Предположим приложение имеет следующую файловую структуру:

vendor/
application/
  app.php
public/
  index.php


Нам необходимо загрузить пакет в каталог vendor и подключить его в файле application/app.php.

Для этого необходимо скачать архив пакета и распаковать его в каталог vendor/bricks_servicelocator/. После этого в его необходимо подключить в файле application/app.php с помощью команды:

require_once('vendor/bricks_servicelocator/Manager.php');
use Bricks\ServiceLocator;

$locator = new ServiceLocator\Manager;


Локатор служб готов к использованию.

Установка с помощью Composer
Composer позволяет устанавливать пакеты Bricks с меньшими "телодвижениями". Все что вам нужно, это добавить файл composer.json в корень вашего проекта и описать следующие зависимости:

{
    "name": "имя проекта",
    "version": "версия",
    "description": "описание",
    "license": "лицензия",
    "repositories": [
      {
        "type": "vcs",
        "url": "https://github.com/Bashka/bricks_servicelocator"
      }
    ],
    "require": {
      "Bashka/bricks_servicelocator": "dev-master"
    }
}


После чего загрузить Composer с помощью команды:

php -r "readfile('https://getcomposer.org/installer');" | php


И установить пакет:

php composer.phar install


В файле application/app.php все объявленные зависимости подключаются одной командой:

require_once('vendor/autoload.php');
use Bricks\ServiceLocator;

$locator = new ServiceLocator\Manager;


Более возвращаться к процессу установки пакетов Bricks я не буду, так как это тривиальный процесс.

Архив с примером.
.
Самостоятельное изучение пакетов Bricks
В будущем я опишу здесь многие из доступных пакетов Bricks, а так же приведу примеры их использования. Если вам хочется самостоятельно приступить к изучению этих пакетов, вы сможете сделать это с помощью документации, которая распространяется вместе с пакетами. Откройте любой пакет Bricks (на пример этот) и прокрутите экран вниз. Вы увидите общее описание пакета, а так же, в разделе "Документация", вы найдете ссылку на файл документации к этому пакету.

Не следует ограничиваться только пользовательской документацией, о которой я только что рассказал. Все PHP классы, которые входят в любой пакет Bricks подробно документированы. Их код максимально прост, потому вам не составит труда читать непосредственно исходные коды, для более глубокого изучения пакета.

Если у вас возникают вопросы по использованию пакетов, вы можете задавать их в этой теме, или создавать обсуждение прямо на GitHub (вы найдете ссылку в разделе "Поддержка").

Так же не бойтесь изменять любой из пакетов Bricks, они распространяются по лицензии GNU GPL v3.0, а это значит, вы можете делать с ними все что угодно (но не продавать).
.
Delphinum
Автозагрузчик ресурсов
Пакет Bricks.Autoload решает довольно тривиальную задачу загрузки ресурсов (классов, файлов, конфигураций приложения) по их имени. Другими словами, этот пакет автоматически загружает требуемые вам классы без необходимости использования команд include/require. Практически все фреймворки включают подобный пакет, так как он сильно упрощает процесс разработки.

Давайте познакомимся с ним на примере приложения вида:

lib/
  MyClass.php
templates/
  index.html
index.php


Основным файлом здесь является index.php, расположенный в корневом каталоге приложения и использующий как класс из файла lib/MyClass.php, так и файл templates/index.html:

// Файл index.php
require_once('lib/MyClass.php');

$obj = new MyClass;
$template = file_get_contents('templates/index.html');
...


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

Bricks.Autoload решает эту проблему довольно просто:

// Файл index.php
require_once('vendor/autoload.php');
use Bricks\Autoload\Loader;

$loader = new Loader;
$loader->map('MyClass', 'lib/MyClass.php');
$loader->map('template', 'templates/template/index.html');

$obj = new MyClass;
$template = file_get_contents($loader->path('template', 'html'));
...


На первый взгляд может показаться что кода стало в разы больше, но вскоре вы убедитесь, что это легко решается. Дайте сначала оценим изменения. Мы подключили автозагрузчик и указали ему пути поиска файлов ресурсов с помощью метода map. Теперь, если вы используете класс, который был перечислен в этом методе (на пример MyClass), он будет автоматически загружен из файла lib/MyClass.php. Более того, вы можете получить адрес файла шаблона index.html по с помощью метода path автозагрузчика.

Расширим наш пример, добавив несколько новых классов и шаблонов:

lib/
  User.php
  News.php
  Comment.php
templates/
  index.html
  user_list.html
  user_edit.html
  news.html
index.php


Перечислять все файлы в методе map будет очень утомительно, потому на этот раз мы воспользуемся методом pref, который позволяет указать не файл, содержащий ресурс, а каталог, в котором его необходимо найти:

// Файл index.php
require_once('vendor/autoload.php');
use Bricks\Autoload\Loader;

$loader = new Loader;
$loader->pref('lib', 'lib');
$loader->pref('tpl', 'templates');

$user = new lib\User;
$template = file_get_contents($loader->path('tpl\user_list', 'html'));
...


Для автоматической загрузки PHP классов он обязательно должны располагаться в соответствующих namespace. Так, класс User из примера должен иметь полное имя lib\User и т.д.

// Файл lib/User.php
namespace lib;

class User{
   ...
}


Если файл можно определить по его имени, использовать методы pref и map не следует. Автозагрузчик самостоятельно найдет файл ресурса и загрузит его:

// Файл index.php
require_once('vendor/autoload.php');
use Bricks\Autoload\Loader;

$loader = new Loader;

$user = new lib\User;
$template = file_get_contents($loader->path('templates\user_list', 'html'));
...


Эти методы нужны только в том случае, если по полному имени ресурса невозможно точно определить его местоположение в файловой системе. На пример:


lib/
  Users/
    User.php
index.php


// Файл lib/Users/User.php
namespace Users;

class User{
   ...
}


Как видно, класс Users\User нельзя преобразовать в адрес файла, его содержащего. В этом случае используется метод map или pref:

// Файл index.php
require_once('vendor/autoload.php');
use Bricks\Autoload\Loader;

$loader = new Loader;
$loader->pref('Users', 'lib/Users');

$user = new Users\User;
...


Метод map менее ресурсоемкий, нежели pref, так как определяет прямое соответствие имени ресурса файлу, его содержащему. Поиск файла ресурса в каталоге требует, хоть и не намного, но все же большей работы от автозагрузчика. Не рекомендуется "экономить" на этом, но об этом так же не следует забывать.

Архив с примером.
.
Delphinum
Комментарий: добавил архивы с примерами к каждой статье.
.
Delphinum
Локатор сервисов
Локация сервисов это по сути аналог шаблона Singleton, но более гибкий и "кошерный". Представим ситуацию: вам необходимо в вашем приложении работать с несколькими таблицами в БД, а так же иметь доступ к конфигурации приложения. Все эти ресурсы, как правило, являются глобально-доступными. Другими словами любой части вашего приложения может потребоваться к ним доступ. С другой стороны, возможно, эти ресурсы необходимо предварительно настроить, прежде чем использовать (к примеру подключиться к БД). Эти задачи как раз решает Bricks.ServiceLocator.

Локатор служб отличается от Singleton тем, что его и все зависимые от него части приложения проще тестировать. Так же этот механизм позволяет полиморфно заменять один ресурс другим, незаметно для системы. К примеру для доступа к БД использовался класс на базе mysqli, но вы решили перейти на PDO. С использованием локатора служб вам потребуется только заменить ресурс доступа к базе, а не "шерстить" весь проект.

Предлагаемый мной пакет очень прост. По сути это ассоциативный массив, которому передаются доступные приложению службы. Рассмотрим пример:

use Bricks\ServiceLocator\Manager;

$locator = new Manager;
$locator->set('config', include('conf/global.conf.php'));
...
$config = $locator->get('config');


В примере демонстрируется работа методов set и get. Первый позволяет зарегистрировать службу под данным именем (в примере это имя config), а второй предоставляет службу по требованию.

Другим достоинством, о котором я уже упоминал ранее, является возможность регистрации фабрик. Фабрика это некоторая функция или метод класса, который будет вызван при обращении к методу get локатора и должен будет сформировать и вернуть требуемый сервис.

Пример:

use Bricks\ServiceLocator\Manager;

$locator = new Manager;
$locator->factory('userTable', function(Manager $locator){
  $conf = $locator->get('config');
  return new PDO('mysql:dbname=user', $conf['db']['user'], $conf['db']['pass']);
});
...
$userTable = $locator->get('userTable');


Здесь фабрика используется для инициализации подключения к базе данных. Важно отметить, что при каждом обращении к этой фабрике, метод будет вызываться заново. На деле это означает, что мы получим несколько подключений к одной и той же таблице. Чтобы этого избежать, достаточно передать true в качестве четвертого параметра метода factory:

use Bricks\ServiceLocator\Manager;

$locator = new Manager;
$locator->factory('userTable', function(Manager $locator){
  $conf = $locator->get('config');
  return new PDO('mysql:dbname=user', $conf['db']['user'], $conf['db']['pass']);
}, null, true);
...
$userTable = $locator->get('userTable');
var_dump($userTable === $locator->get('userTable')); // true


Архив с примером.
.
Роутер
Маршрутизация запросов это один из основных механизмов любого фреймворка, обеспечивающий стройную структуру приложения и разделение ответственности. Чаще всего используется маршрутизация на основе парсинга URI, на пример так:

/admin/user/delete/([0-9]+)


Такой парсер может использоваться для удаление пользователей по его идентификатору.

В пакете Bricks.Http.Router реализована RESTful маршрутизация. Это означает, что применяются не только ставшие стандартными для PHP запросы типа GET и POST, но так же PUT и DELETE. Это полезно для реализации одно-страничных Web-приложений.

Пакет включает два управляющих класса:
Request - представление запроса
Response - представление ответа

Рассмотрим пример с их использованием:
use Bricks\Http\Routing\Request;

$request = new Request;
echo $request->method(); // Метод запроса (GET, POST, PUT или DELETE)
echo $request->path(); // URI запроса
echo $request->header('Content-Type'); // Параметр заголовка запроса
echo $request->cookie('login'); // Параметр cookie
echo $request->param('action'); // Параметр action запроса


use Bricks\Http\Routing\Response;

$response = new Response;
$response->code(200); // Код ответа
$response->header('Content-Type', 'text/html'); // Заголовок ответа
$response->cookie('role', 'user'); // Cookie ответа
$response->body('<div>Hello world</div>'); // Тело ответа
$response->send(); // Передача ответа клиенту


Думаю пример в объяснении не нуждается.

Роутинг запросов выполняется с помощью класса Router. С помощью этого класса возможно зарегистрировать любое количество маршрутов и действий, которые с ними связаны.

Пример регистрации маршрута:

use Bricks\Http\Routing\Router;

$router = new Router;
$router->get('~^/admin/([A-Za-z]+)/([A-Za-z]+)~', function(Request $request, Response $response, array $match){
  list($controller, $action) = $match;
  $controller = new $controller;
  $controller->$action();
});

$response = new Response;
$router->run(new Request, $response);
$response->send();


Здесь регистрируется обработка GET запросов к пути вида:

/admin/контроллер/действие


Функция, передаваемая во втором параметре метода get будет вызвана, если данный маршрут будет являться успешным. Эта функция получит в качестве параметров экземпляры классов Request и Response, что позволит ей сформировать ответ клиенту, а так же массив $match, содержащий группы, выделенные в URI шаблоном маршрута (в данном случае это имя целевого контроллера и действие).

Класс Router включает следующие методы маршрутизации:

get - обработка GET запросов
post - обработка POST запросов
put - обработка PUT запросов
delete - обработка DELETE запросов
all - обработка запросов любого типа

Архив с примером.
Всего: 49