Собственный PHP-фреймворк: базовый модуль

40
use imp\std; Базовый модуль фреймворка Interlabs 10 ноября 2014 1 / 40
  • date post

    16-Jun-2015
  • Category

    Technology

  • view

    151
  • download

    1

description

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

Transcript of Собственный PHP-фреймворк: базовый модуль

Page 1: Собственный PHP-фреймворк: базовый модуль

use imp\std;Базовый модуль фреймворка

Interlabs

10 ноября 2014

1 / 40

Page 2: Собственный PHP-фреймворк: базовый модуль

Что это?imp\std

• единые соглашения о кодировании• единая стратегия обработки ошибок• небольшая базовая библиотека классов

Основа для:

• imp\http — фреймворк для HTTP приложений• imp\data — собственный ORM• imp\template — простая шаблонизация• и всего остального

2 / 40

Page 3: Собственный PHP-фреймворк: базовый модуль

Приоритеты

• максимально простая структура классов и API• максимальное единообразие API различных модулей• минимум зависимостей от внешних модулей и сложныхинструментов

• минимально достаточные dependency injection и events• если необходимо — компенсация кривизны PHP API• простота — документация дополняет код, а не наоборот• важна не полнота базового API, а его дальнейшаярасширяемость

3 / 40

Page 4: Собственный PHP-фреймворк: базовый модуль

Стиль кодирования

4 / 40

Page 5: Собственный PHP-фреймворк: базовый модуль

Явные зависимостиЯвно декларируем и документируем все зависимости классов,документируя и упрощая рефакторинг:

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

Page 6: Собственный PHP-фреймворк: базовый модуль

Описание зависимостейОписываем каждую зависимость в обозначениях PlantUML:

• общая UML-диаграмма классов строится простым скриптом• сразу можно получить представление о зависимостях

http://www.plantuml.comсуперинструмент для построения UML по простой текстовой разметке

<|-- наследование <.. зависимость<|.. реализация <-- ассоциация

o-- агрегация*-- композиция

6 / 40

Page 7: Собственный PHP-фреймворк: базовый модуль

Диаграмма зависимостей

Наглядно, не требуeт сложного формата документирования и больших затрат.

7 / 40

Page 8: Собственный PHP-фреймворк: базовый модуль

Стандарт кодированияВ коде должно быть легко ориентироваться в обычномредакторе, без IDE.

• один класс — один файл• определение класса из нескольких частей, разделяем их вкоде чтобы быстрее ориентироваться

• обязательно разделяем по виду доступа (public,protected, private)

• дополнительно разделяем по функционалу илиреализуемому интерфейсу

• используем пустые строки для разделения отдельныхблоков кода

8 / 40

Page 9: Собственный PHP-фреймворк: базовый модуль

Структурирование кода// 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

Page 10: Собственный PHP-фреймворк: базовый модуль

Документирование

• лучше примитивное документирование, чем никакого• главный фактор — время и удобство чтения кода• формальный подход javadoc — затратно поддерживать,тяжелый для чтения исходник

• документация не должна загромождать код и должналегко читаться непосредственно в коде

Пишем README.md в корне каждого модуля, документируемклассы, методы и переменные обычными текстовымикомментариями.

10 / 40

Page 11: Собственный PHP-фреймворк: базовый модуль

Документирование

// Простейший 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

Page 12: Собственный PHP-фреймворк: базовый модуль

ДокументированиеДокументирование не должно усложнять работу с исходнымфайлом:

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

• простой формат описаний способствует их написанию• в сочетании с группировкой методов и описаниемзависимостей упрощает чтение исходников.

12 / 40

Page 13: Собственный PHP-фреймворк: базовый модуль

Контроль типовimp\std\Assert

13 / 40

Page 14: Собственный PHP-фреймворк: базовый модуль

Контроль типовКонтроль типов уменьшает количество ошибок.Типизированные аргументы — это хорошо, но не всегдадостаточно:

• иногда нужен контроль скалярных значений:imp\std\Assert

• иногда нужен контроль возвращаемых значений:методы assert() в классах

• иногда нужно явное преведение типа,обычно к (string) или (int)

14 / 40

Page 15: Собственный PHP-фреймворк: базовый модуль

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

Page 16: Собственный PHP-фреймворк: базовый модуль

Методы 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

Page 17: Собственный PHP-фреймворк: базовый модуль

Приведение типов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

Page 18: Собственный PHP-фреймворк: базовый модуль

Обработка ошибокimp\std\exceptions

18 / 40

Page 19: Собственный PHP-фреймворк: базовый модуль

Только исключения• собственные классы всегда генерируют исключения вслучае ошибок

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

• модуль генерирует только собственныеruntime-исключения, обрабатывая, если нужно,исключения других модулей

• поэтому удобно, если каждый модуль определяетсобственный производный класс для ошибок временивыполнения

• логические ошибки — глобально на уровне приложения

19 / 40

Page 20: Собственный PHP-фреймворк: базовый модуль

Стандартные исключения\LogicException \RuntimeException

imp\std\exceptions

LogicException RuntimeExceptionAssertionFailedException ContainerExceptionInvalidKeyException OutOfBoundsExceptionInvalidOperationException InvalidValueExceptionInvalidPropertyExceptionInvalidTypeExceptionNotImplementedException

// Возможность форматирования сообщения об ошибках:throw new InvalidPropertyException(array(

’Unsupported property %s’, $property));

20 / 40

Page 21: Собственный PHP-фреймворк: базовый модуль

Исключения модулей• стандартное подпространство имен exceptions• базовый класс runtime-исключения, наследуемый отimp\std\exceptionsRuntimeException

• производные классы — только по необходимости• прежде чем создавать новый класс — думаем, кем онбудет обрабатываться

namespace imp\data\exceptions;

// DEPENDENCIES ///////////////////////////////////////////////////////

use imp\std\exceptions\RuntimeException; // <!-- наследует

// CLASS //////////////////////////////////////////////////////////////

class DataException extends RuntimeException {}

21 / 40

Page 22: Собственный PHP-фреймворк: базовый модуль

Базовые классы

22 / 40

Page 23: Собственный PHP-фреймворк: базовый модуль

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

Page 24: Собственный PHP-фреймворк: базовый модуль

DI-контейнер (124 LOC)

use imp\std\Container;

• значения возвращаются как есть• сервисы — замыкание выполняется при первом вызове,при последующих — результат выполнения замыкания

• фабрики — замыкание выполняется каждый раз,возвращается результат

• элементы могут быть присвоены только один раз и немогут быть удалены

• контейнер может делегировать родительскому контейнеру• для элементов можно ввести иерархическое именованиеиспользуя строки с разделителем

• базовый класс для HTTP, CLI приложений и слоя модели

24 / 40

Page 25: Собственный PHP-фреймворк: базовый модуль

Иерархия контейнеровКонтейнер может делегировать родителю и это очень полезно:

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

Page 26: Собственный PHP-фреймворк: базовый модуль

События (116 LOC)

use imp\std\Event;

• терминология: обработчик события == действие (action)• инициируются объектом, поддерживающим события• каждый модуль определяет набор поддерживаемыхсобытий в виде отдельного класса

• каждый объект содержит список действий, который могутформировать иерархию

• для каждого действия определяется интерфейс, но можноиспользовать и замыкания

• действия для одного события выстраиваются в цепочку• каждый обработчик явно вызывает следующий,передаваемый ему в качестве аргумента

26 / 40

Page 27: Собственный PHP-фреймворк: базовый модуль

События: пример// Событи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

Page 28: Собственный PHP-фреймворк: базовый модуль

Иерархия действий

28 / 40

Page 29: Собственный PHP-фреймворк: базовый модуль

Декларация событийМодуль описывает поддерживаемый набор событий в видекласса, определяющего константы-идентификаторы событий:

final class DBIEvents {const EXECUTE = ’imp\dbi\actions\ExecuteAction’;

}

Модуль определяет действия в виде интерфейсов:

namespace imp\dbi\actions;interface ExecuteAction {

function __invoke(Cursor $cursor, array $args, $yield);}

Имя события = имя интерфейса соответствующего действия.29 / 40

Page 30: Собственный PHP-фреймворк: базовый модуль

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

Page 31: Собственный PHP-фреймворк: базовый модуль

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

Page 32: Собственный PHP-фреймворк: базовый модуль

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

Page 33: Собственный PHP-фреймворк: базовый модуль

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

Page 34: Собственный PHP-фреймворк: базовый модуль

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

Page 35: Собственный PHP-фреймворк: базовый модуль

Потокиimp\std\io

Минимальная обертка над встроенными функциями (fopen(),fread(), fwrite() и т.д.):

• единообразная обработка ошибок через исключениястандартных классов

• интерфейс итераторов• возможность расширения API в будущем

imp\std\IO — дополнительный процедурный API

35 / 40

Page 36: Собственный PHP-фреймворк: базовый модуль

Потоки: 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

Page 37: Собственный PHP-фреймворк: базовый модуль

Потоки: использование// Вывод в 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

Page 38: Собственный PHP-фреймворк: базовый модуль

imp\std\interfacesНаследуем некоторые стандартные интерфейсы на случайпоследующего расширения, определяем недостающие:

• ArrayAccess — стандартный доступ по индексу,наследуется от ArrayAccess

• MethodAccess — интерфейс-метка для объектов спредпочтительным доступом через методы

• PropertyAccess — стандартный доступ черездинамические свойства (__get, __set и т.д.)

• Runnable — интерфейс-метка для выполняемого объекта(__invoke())

• JSONSerializable — сериализация в JSON(jsonSerialize())

38 / 40

Page 39: Собственный PHP-фреймворк: базовый модуль

imp\std\iterators

Определяем несколько полезных классов итераторов, потомбудет больше:

• FilterIterator — итерирование с фильтрацией• KeyRangeIterator — итерирование по подмножествуиндексов

• MapIterator — итерирования по результату выполнениядля каждого элемента

39 / 40

Page 40: Собственный PHP-фреймворк: базовый модуль

privatehttps://bitbucket.org/interlabs/imp/

use imp\std!

40 / 40