Собственный PHP-фреймворк: базовый модуль
-
date post
16-Jun-2015 -
Category
Technology
-
view
151 -
download
1
description
Transcript of Собственный PHP-фреймворк: базовый модуль
use imp\std;Базовый модуль фреймворка
Interlabs
10 ноября 2014
1 / 40
Что это?imp\std
• единые соглашения о кодировании• единая стратегия обработки ошибок• небольшая базовая библиотека классов
Основа для:
• imp\http — фреймворк для HTTP приложений• imp\data — собственный ORM• imp\template — простая шаблонизация• и всего остального
2 / 40
Приоритеты
• максимально простая структура классов и API• максимальное единообразие API различных модулей• минимум зависимостей от внешних модулей и сложныхинструментов
• минимально достаточные dependency injection и events• если необходимо — компенсация кривизны PHP API• простота — документация дополняет код, а не наоборот• важна не полнота базового API, а его дальнейшаярасширяемость
3 / 40
Стиль кодирования
4 / 40
Явные зависимостиЯвно декларируем и документируем все зависимости классов,документируя и упрощая рефакторинг:
namespace imp\data;
// DEPENDENCIES ///////////////////////////////////////////////////////
use ArrayAccess; // <|.. реализуетuse Countable; // <|.. реализуетuse IteratorAggregate; // <|.. реализует
use imp\data\Kind; // o-- относится кuse imp\data\Row; // o-- содержитuse imp\data\Model; // o-- используетuse imp\data\Entity; // <.. создаетuse imp\data\Relation; // <.. используетuse ArrayIterator; // <.. создаетuse imp\data\exceptions\DataException; // <.. генерируетuse imp\std\exceptions\InvalidOperationException; // <.. генерирует
5 / 40
Описание зависимостейОписываем каждую зависимость в обозначениях PlantUML:
• общая UML-диаграмма классов строится простым скриптом• сразу можно получить представление о зависимостях
http://www.plantuml.comсуперинструмент для построения UML по простой текстовой разметке
<|-- наследование <.. зависимость<|.. реализация <-- ассоциация
o-- агрегация*-- композиция
6 / 40
Диаграмма зависимостей
Наглядно, не требуeт сложного формата документирования и больших затрат.
7 / 40
Стандарт кодированияВ коде должно быть легко ориентироваться в обычномредакторе, без IDE.
• один класс — один файл• определение класса из нескольких частей, разделяем их вкоде чтобы быстрее ориентироваться
• обязательно разделяем по виду доступа (public,protected, private)
• дополнительно разделяем по функционалу илиреализуемому интерфейсу
• используем пустые строки для разделения отдельныхблоков кода
8 / 40
Структурирование кода// DEPENDENCIES ////////////////////////////////////////
use ArrayAccess;
// CLASS ///////////////////////////////////////////////
class Collection implement ArrayAccess{// STATE ///////////////////////////////////////////////
var $items = array();
// PUBLIC //////////////////////////////////////////////
public function __construct() { ... }
// ArrayAccess /////////////////////////////////////////
public function offsetGet($index) { ... }
// PRIVATE /////////////////////////////////////////////
private function addElement($key) { ... }
}
Всегда группируем методы по области видимости и реализуемому протоколу.9 / 40
Документирование
• лучше примитивное документирование, чем никакого• главный фактор — время и удобство чтения кода• формальный подход javadoc — затратно поддерживать,тяжелый для чтения исходник
• документация не должна загромождать код и должналегко читаться непосредственно в коде
Пишем README.md в корне каждого модуля, документируемклассы, методы и переменные обычными текстовымикомментариями.
10 / 40
Документирование
// Простейший DI-контейнер/локатор.//// Идея — Pimple (https://github.com/fabpot/Pimple).//class Container implements ArrayAccess{// STATE ///////////////////////////////////////////////////////////////////////
private $parent; // - родительский контейнерprivate $factories = array(); // - фабрики элементовprivate $values = array(); // - значения элементов
// PUBLIC //////////////////////////////////////////////////////////////////////
// Конструктор.//public function __construct(
array $config = array(), // - исходная конфигурацияContainer $parent = null // - родительский контейнер
) {$this->parent = $parent;$this->setup($config);
}
11 / 40
ДокументированиеДокументирование не должно усложнять работу с исходнымфайлом:
• обычные текстовые коментарии• абзацы отделяются пустыми строками• первый абзац — краткое описание• в методах каждый аргумент на отдельной строке• пояснения к аргументам и переменным — в той же строке,что и определение
• простой формат описаний способствует их написанию• в сочетании с группировкой методов и описаниемзависимостей упрощает чтение исходников.
12 / 40
Контроль типовimp\std\Assert
13 / 40
Контроль типовКонтроль типов уменьшает количество ошибок.Типизированные аргументы — это хорошо, но не всегдадостаточно:
• иногда нужен контроль скалярных значений:imp\std\Assert
• иногда нужен контроль возвращаемых значений:методы assert() в классах
• иногда нужно явное преведение типа,обычно к (string) или (int)
14 / 40
imp\std\Assert
use imp\std\Assert;
$value = Assert::int($value);$value = Assert::float($value);$value = Assert::numeric($value);$value = Assert::string($value);$value = Assert::object($value);$value = Assert::objectOrNull($value);$value = Assert::runnable($value);$value = Assert::runnableOrNull($value);$value = Assert::resource($value);$value = Assert::scalar($value);
Если значение не соответствует типу, генерируетсяimp\std\exceptions\InvalidTypeException
15 / 40
Методы assert()Необходимо удостовериться, что значение принадлежитнекоторому классу, например, для результата вызова метода,или для массива объектов:
class MyClass{
static public function assert(MyClass $object // - проверяемое значение
) {return $object;
}
// ...}
$result = MyClass::assert($someObject->someMethod());foreach ($objects as $o) {
MyClass::assert($o)->method();}
16 / 40
Приведение типовclass ResourceStream extends Stream{
public function __construct($uri, // - URI ресурса$mode // - режима работы
) {$uri = (string) $uri;$mode = (string) $mode;...
}}$byPath = new ResourceStream(’path/to/file’, ’r’);$file = FS::at(’/path/to/file’);$byFile = new ResourceStrean($file, ’r’);
• (string) — имеет смысл в большинстве случаев• (array) — очень осторожно, если не объект
17 / 40
Обработка ошибокimp\std\exceptions
18 / 40
Только исключения• собственные классы всегда генерируют исключения вслучае ошибок
• для функций, которые так не делают — врапперы• все модули используют стандартный набор исключенийдля логических ошибок
• модуль генерирует только собственныеruntime-исключения, обрабатывая, если нужно,исключения других модулей
• поэтому удобно, если каждый модуль определяетсобственный производный класс для ошибок временивыполнения
• логические ошибки — глобально на уровне приложения
19 / 40
Стандартные исключения\LogicException \RuntimeException
imp\std\exceptions
LogicException RuntimeExceptionAssertionFailedException ContainerExceptionInvalidKeyException OutOfBoundsExceptionInvalidOperationException InvalidValueExceptionInvalidPropertyExceptionInvalidTypeExceptionNotImplementedException
// Возможность форматирования сообщения об ошибках:throw new InvalidPropertyException(array(
’Unsupported property %s’, $property));
20 / 40
Исключения модулей• стандартное подпространство имен exceptions• базовый класс runtime-исключения, наследуемый отimp\std\exceptionsRuntimeException
• производные классы — только по необходимости• прежде чем создавать новый класс — думаем, кем онбудет обрабатываться
namespace imp\data\exceptions;
// DEPENDENCIES ///////////////////////////////////////////////////////
use imp\std\exceptions\RuntimeException; // <!-- наследует
// CLASS //////////////////////////////////////////////////////////////
class DataException extends RuntimeException {}
21 / 40
Базовые классы
22 / 40
DI-контейнер kind of ,
Основная идея — Pimple, но API (субъективно) логичнее:
$c = new Container([ // значения, определяющие первоначальную’dsn’ => ’mysql://localhost’, // конфигурацию, без сервисов и фабрик.
]);
$c->service(’db’, function ($c) { // сервис создается только один разreturn new Connection($c[’dsn’]); // при первом обращении
});
$c->factory(’table’, function ($c, $name) { // фабрика при каждом вызовеreturn new Table($c[’db’], $name); // создает новый объект
});
$dsn = $c[’dsn’]; // - строка DSN$db = $c[’db’]; // - Connection в одном экземпляре$items = $c->create(’table’, ’items’); // - каждый раз новый Table$factory = $c[’table’]; // - фабричное замыкание для Table
23 / 40
DI-контейнер (124 LOC)
use imp\std\Container;
• значения возвращаются как есть• сервисы — замыкание выполняется при первом вызове,при последующих — результат выполнения замыкания
• фабрики — замыкание выполняется каждый раз,возвращается результат
• элементы могут быть присвоены только один раз и немогут быть удалены
• контейнер может делегировать родительскому контейнеру• для элементов можно ввести иерархическое именованиеиспользуя строки с разделителем
• базовый класс для HTTP, CLI приложений и слоя модели
24 / 40
Иерархия контейнеровКонтейнер может делегировать родителю и это очень полезно:
use imp\http\app\Application; // - HTTP-приложениеuse imp\data\Model; // - Сервис слоя моделиuse imp\format\JSON; // - JSON-модульuse imp\dbi\Connection; // - Сервис подключения к БД
$app = new Application(JSON::load(’config.json’)); // - загрузка конфигурации$app->service(’dbi.connection’, function ($app) { // - сервис БД
return new Connection($app[’dbi.dsn’], $app[’dbi.user’], $app[’dbi.password’])
});
// Модель имеет доступ ко всем параметрам конфигурации приложения,// через делегирование, и в то же время не связана с приложением.// Можно независимо сконфигурировать для тестирования, CLI-приложения и т.д.$app->service(’model’, function ($app) { // - сервис модели
return new Model(JSON::load(’model.json’), $app[’model.sequence’], $app);
});25 / 40
События (116 LOC)
use imp\std\Event;
• терминология: обработчик события == действие (action)• инициируются объектом, поддерживающим события• каждый модуль определяет набор поддерживаемыхсобытий в виде отдельного класса
• каждый объект содержит список действий, который могутформировать иерархию
• для каждого действия определяется интерфейс, но можноиспользовать и замыкания
• действия для одного события выстраиваются в цепочку• каждый обработчик явно вызывает следующий,передаваемый ему в качестве аргумента
26 / 40
События: пример// Событиe для конкретного соединения:$connection->onExecute(function (Cursor $cursor, array $args, $yield) {
Log::info("Ready to execute query %s", (string) $cursor);}
);
// Глобальное событие выполнение запроса:Event::on(
DBIEvents::EXECUTE, function (Cursor $cursor, array $args, $yield) {$r = $yield($cursor, $args);Log::info("Query ready");return $r;
});$cursor = $connection->prepare(’SELECT * FROM table’);
// Событие выполнение конкретного курсора.$cursor->onExecute(function (Cursor $cursor, array $args, $yield) {
Log::info("This is very special query");return $yield($cursor, $args);
});
27 / 40
Иерархия действий
28 / 40
Декларация событийМодуль описывает поддерживаемый набор событий в видекласса, определяющего константы-идентификаторы событий:
final class DBIEvents {const EXECUTE = ’imp\dbi\actions\ExecuteAction’;
}
Модуль определяет действия в виде интерфейсов:
namespace imp\dbi\actions;interface ExecuteAction {
function __invoke(Cursor $cursor, array $args, $yield);}
Имя события = имя интерфейса соответствующего действия.29 / 40
imp\std\util• cтандартный API PHP неструктурирован и часто неудобен• но мы не переписываем его• просто добавляем необходимые функции• но структурируем их, разбивая на отдельные статическиеклассы и делая возможным дальнейшее развитие.
Сейчас:
imp\std\util\S строки
imp\std\util\A массивы
imp\std\util\F функции
imp\std\util\O объекты
Будет:
imp\std\util\R pregexp
imp\std\util\C коллекции
Первоначальная структура для дальнейшего расширения. 30 / 40
SОсновная проблема — UTF8:
• часть встроенных функций UTF8-safe• большинство — через mbstring mb_*()• некоторые полезные функции отсутствуют в mbstring• проще сделать свой расширяемый враппер
S::length() last() rtrim() slugify()concat() hasPrefix() toupper() strBefore()concatWith() unprefix() tolower() strAfter()replace() hasSuffix() ucfirst() splitBy()substr() unsuffix() lcfirst()contains() trim() capitalize()first() ltrim() asciify()
31 / 40
A, F, 0 — пока необходимый минимумimp\std\util\A
• at() - получение значения с умолчанием• values() - получений нужного количества значений• first() - получение первого элемента• last() - получение последнего элемента• firstKey() - получение первого индекса• lastKey() - получение последнего индекса• hash() - расчет хеша содержимого массива
imp\std\util\F
• bind() — аналог Closure::bind() с учетом версии PHP• notning() — пустой closure для композиции функций
imp\std\util\O
• set()/get() — работа со свойствами (индексы, методы, свойства)32 / 40
imp\std\PHPВспомогательные функции, относящиеся к языку в целом:
• version() — возвращает или проверяет версию PHP• with() — возвращает переданный аргумент для обходапроблем синтаксиса в ранних версиях
• typeOf() — возвращает тип для скалярных значений имякласса для объектов
• struct() — псевдоним для return (object) array(...)
return PHP::version(’5.4’)? Closure::bind($closure, $object, $object): $closure;
throw new InvalidTypeException([’Bad type %s’, PHP::typeOf($v)
]);33 / 40
imp\std\timeПростые врапперы для DateTime, DateInterval, DateTimeZone:
• возможность использовать Unix time без @ в конструкторе• автоматическое преобразование в строку (__toString)• поддержка jsonSerialize()• возможность добавления недостающего API в будущем
Дополнительно процедурный API imp\std\Time:
$datetime = Time::at(’2014-10-30 11:23:13’);$datetime = Time::at(246056400);$datetime = Time::UTC();$datetime = Time::local();$timezone = Time::timezone(’Europe/Moscow’);
34 / 40
Потокиimp\std\io
Минимальная обертка над встроенными функциями (fopen(),fread(), fwrite() и т.д.):
• единообразная обработка ошибок через исключениястандартных классов
• интерфейс итераторов• возможность расширения API в будущем
imp\std\IO — дополнительный процедурный API
35 / 40
Потоки: API
imp\std\io\Stream <|-- imp\std\io\ResourceStream__construct($id) __construct($uri, $mode)__desctruct()close()read($length) imp\std\IOgets($limit = null) open($uri, $mode)write($data, $length = null) byLine($stream)puts($str) by($length, $stream)format($format) stdin()formatArgs($format, array $args) stdout()eof() stderr()getId()getWritten()
36 / 40
Потоки: использование// Вывод в stderr:IO::stderr()->format(’Hello, %s’, $name);
$readme = IO::open(’README.txt’, ’r’);
// Построчное чтение:while ($line = $readme->gets()) {
print $line;}
// Построчное чтение через итератор:foreach (IO::byLine($readme) as $line) {
print $line;}
foreach (IO::by(255, $stream) as $fragment) {// ...
}
37 / 40
imp\std\interfacesНаследуем некоторые стандартные интерфейсы на случайпоследующего расширения, определяем недостающие:
• ArrayAccess — стандартный доступ по индексу,наследуется от ArrayAccess
• MethodAccess — интерфейс-метка для объектов спредпочтительным доступом через методы
• PropertyAccess — стандартный доступ черездинамические свойства (__get, __set и т.д.)
• Runnable — интерфейс-метка для выполняемого объекта(__invoke())
• JSONSerializable — сериализация в JSON(jsonSerialize())
38 / 40
imp\std\iterators
Определяем несколько полезных классов итераторов, потомбудет больше:
• FilterIterator — итерирование с фильтрацией• KeyRangeIterator — итерирование по подмножествуиндексов
• MapIterator — итерирования по результату выполнениядля каждого элемента
39 / 40
privatehttps://bitbucket.org/interlabs/imp/
use imp\std!
40 / 40