Хабрахабр

Спецификации в PHP

Happyr Doctrine Specification

Кратко о спецификациях:

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

Это RulerZ и Happyr Doctrine Specification. На сегодня существует два (если знаете другие проекты, напишите пожалуйста в комментариях) успешных и популярных проекта на PHP, позволяющих описывать бизнес-правила в спецификациях и фильтровать наборы данных. Сравнение этих проектов потянет на целую статью. Оба проекта являются мощными инструментами со своими преимуществами и недостатками. Здесь же я хочу рассказать, что нам привнес новый релиз в Doctrine Specification.

Те, кто в той или иной степени знакомы с проектом, могут смело пропустить этот раздел.

Полученные композиции можно свободно реиспользовать, и комбинировать в ещё более сложные композиции, которые легко тестировать. С помощью этого проекта можно описывать спецификации в виде объектов, составляя из них композицию и, тем самым, составлять сложные бизнес-правила. По сути, Doctrine Specification — это уровень абстракции над Doctrine ORM QueryBuilder и Doctrine ORM Query. Спецификации Doctrine Specification используются для построения запросов Doctrine.

Спецификации применяются через Doctrine Repository:

$result = $em->getRepository(MyEntity::class)->match($spec);

Спецификацию можно применить и вручную, но это не особо удобно и по большому счету бессмысленно.

$spec = ...
$alias = 'e';
$qb = $em->getRepository(MyEntity::class)->createQueryBuilder($alias);
$spec->modify($qb, $alias);
$filter = (string) $spec->getFilter($qb, $alias);
$qb->andWhere($filter);
$result = $qb->getQuery()->execute();

В репозитории есть несколько методов:

  • match — получение всех результатов соответствующих спецификации;
  • matchSingleResult — эквивалент Query::getSingleResult();
  • matchOneOrNullResult — эквивалент matchSingleResult, но разрешает вернуть null;
  • getQuery — создаёт QueryBuilder, применив к нему спецификацию и возвращает объект Query из него.

С недавних пор к ним добавилися метод getQueryBuilder, который создаёт QueryBuilder и, применив к нему спецификацию, возвращает его.

В проекте выделяются несколько типов спецификаций:

Логические спецификации

Спецификации andX и orX так же выполняют роль коллекции спецификаций.

  • Spec::andX()
  • Spec::orX()
  • Spec::not()

Можно в явном виде инстациировать объект спецификации: Инстациировать объекты библиотечных спецификаций принято через фасад Spec, но это не обязательно.

new AndX();
new OrX():
new Not();

Фильтрующие спецификации

К ним относятся операции сравнения: Фильтрующие спецификации, собственно, и составляют правила бизнес-логики и используются в WHERE запроса.

  • isNull — эквивалент SQL IS NULL
  • isNotNull — эквивалент SQL IS NOT NULL
  • in — эквивалент IN ()
  • notIn — эквивалент NOT IN ()
  • eq — проверка на равенство =
  • neq — проверка на неравенство !=
  • lt — меньше чем <
  • lte — меньше или равно <=
  • gt — больше чем >
  • gte — больше или равно >=
  • like — эквивалент SQL LIKE
  • instanceOfX — эквивалент DQL INSTANCE OF

Пример использования фильтрующий спецификаций:

$spec = Spec::andX( Spec::eq('ended', 0), Spec::orX( Spec::lt('endDate', new \DateTime()), Spec::andX( Spec::isNull('endDate'), Spec::lt('startDate', new \DateTime('-4 weeks')) ) )
);

Модификаторы запроса

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

  • join
  • leftJoin
  • innerJoin
  • limit
  • offset
  • orderBy
  • groupBy
  • having

Он объединяет в себе функции limit и offset и сам высчитывает offset исходя из размера слайса и его порядкового номера. Хочу отдельно отметить модификатор slice. Создавая модификатор я преследовал цель упрощения конфигурирования спецификаций при пагинации. В реализации этого модификатора мы разошлись во мнениях с автором проекта. Но автор проекта посчитал правильным начинать отсчёт в стиле программирования, то есть с 0. В этом контексте первая страница с порядковым номером 1 должна была быть эквивалентна первому слайсу с порядковым номером 1. Пэтому стоит помнить, что если вам нужен первый слайс, вам необходимо указывать 0 в качестве порядкового номера.

Модификаторы результата

Они применяются к Doctrine Query. Модификаторы результата существуют немного отдельно от спецификаций. Следующие модификаторы управляют гидрацией данных (Query::setHydrationMode()):

  • asArray
  • asSingleScalar
  • asScalar

Модификатор cache управляет кэшированием результата запроса.

Он помогает решить проблемы с кэшированием, когда нужно работать с бизнес-правилами, требующими сравнивать какие-то значения с текущим временем. Отдельно стоит упомянуть модификатор roundDateTimeParams. Решить эту проблему призван модификатор roundDateTimeParams. Это нормальные бизнес-правила, но из-за того, что время не постоянная величина, у вас не будет работать кэширование более чем на одну секунду. То есть, если мы хотим закэшировать запрос на 10 минут, мы используем Spec::cache(600) и Spec::roundDateTimeParams(600). Он проходится по всем параметрам запроса, ищет в них дату и округляет ее до заданного значения в нижнюю сторону, что даёт нам значения даты всегда кратные одному значению и мы не получим дату в будущем. Изначально предлагалось объединить эти два модификатара ради удобства, но решено было их разделить ради SRP.

Единственная предустановленная спецификация это countOf позволяющая получить количество сущностей соответствующее спецификации. В Happyr Doctrine-Specification для спецификаций выделен отдельный интерфейс который объединяет в себе фильтр и модификатор запроса. Для создания собственных спецификаций принято расширять абстрактный класс BaseSpecification.

В репозиторий добавились новые методы:

  • matchSingleScalarResult — эквивалент Query::getSingleScalarResult();
  • matchScalarResult — эквивалент Query::getScalarResult();
  • iterate — эквивалент Query::iterate().

Добавлена спецификация MemberOfX — эквивалент DQL MEMBER OF и добавлен модификатор запроса indexBy — эквивалент QueryBuilder::indexBy().

Операнды

Все условия в фильтрах состоят из левого, правого операндов и оператора между ними. В новом релизе введено понятие Операнд.

<left_operand> <operator> <right_operand>

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

  • Невозможно использовать функции;
  • Невозможно использовать псевдонимы для полей;
  • Невозможно сравнить два поля;
  • Невозможно сравнить два значения;
  • Невозможно использовать арифметическое операции;
  • Невозможно указать тип данных для значения (value).

Это открывает много возможностей и делает фильтры более простыми. В новой версии фильтрам в аргументах передаются объекты операнды и трансформация их в DQL делегируется самим операндам.

Поле и значение

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

// DQL: e.day > :day
Spec::gt('day', $day);
// or
Spec::gt(Spec::field('day'), $day);
// or
Spec::gt(Spec::field('day', $dqlAlias), $day);

// DQL: e.day > :day
Spec::gt('day', $day);
// or
Spec::gt('day', Spec::value($day));
// or
Spec::gt('day', Spec::value($day, Type::DATE));

Можно сравнивать 2 поля:

// DQL: e.price_current < e.price_old
Spec::lt(Spec::field('price_current'), Spec::field('price_old'));

Можно сравнить 2 поля разных сущностей:

// DQL: a.email = u.email
Spec::eq(Spec::field('email', 'a'), Spec::field('email', 'u'));

Арифметические операции

Для примера рассмотрим рассчёт очков пользователя: Добавлена поддержка стандартных арифметических операций -+*/%.

// DQL: e.posts_count + e.likes_count > :user_score
Spec::gt( Spec::add(Spec::field('posts_count'), Spec::field('likes_count')), $user_score
);

Арифметические операции можно вкладывать одни в другие:

// DQL: ((e.price_old - e.price_current) / (e.price_current / 100)) > :discount
Spec::gt( Spec::div( Spec::sub(Spec::field('price_old'), Spec::field('price_current')), Spec::div(Spec::field('price_current'), Spec::value(100)) ), Spec::value($discount)
);

Функции

Их можно использовать как статические методы класса Spec, так и через метод Spec::fun(). В новом релизе добавились операнды с функциями.

// DQL: size(e.products) > 2
Spec::gt(Spec::size('products'), 2);
// or
Spec::gt(Spec::fun('size', 'products'), 2);
// or
Spec::gt(Spec::fun('size', Spec::field('products')), 2);

Функции могут быть вложенным одна в другую:

// DQL: trim(lower(e.email)) = :email
Spec::eq(Spec::trim(Spec::lower('email')), trim(strtolower($email)));
// or
Spec::eq( Spec::fun('trim', Spec::fun('lower', Spec::field('email'))), trim(strtolower($email))
);

Аргументы для функций можно передавать как отдельные аргументы, так и передав их в массиве:

// DQL: DATE_DIFF(e.create_at, :date)
Spec::DATE_DIFF('create_at', $date);
// or
Spec::DATE_DIFF(['create_at', $date]);
// or
Spec::fun('DATE_DIFF', 'create_at', $date);
// or
Spec::fun('DATE_DIFF', ['create_at', $date]);

Управление выборкой

Например: Иногда нужно управлять списком возвращаемых значений.

  • Добавить в результат ещё одну сущность, чтобы не делать подзапросы для получения связей;
  • Возращать не всю сущность, а только набор отдельных полей;
  • Использовать псевдонимы;
  • Использовать скрытые псевдонимы с условиями для сортировки (так требует Doctrine, но обещают исправить).

8. До версии 0. Начиная с версии 0. 0 для выполнения этих задач требовалось создавать свои спецификации для этих нужд. 0 можно воспользоваться методом getQueryBuilder() и уже через интерфейс QueryBuilder управлять выборкой. 8.

0. В новом релизе 1. select полностью заменяет список выбираемых значений, а addSelect добавляет к списку новые значения. 0 добавились модификаторы запроса select и addSelect. Таким образом можно расширять возможности библиотеки под свои нужды. В качестве значения можно использовать объект реализующий интерфейс Selection или фильтр. Рассмотрим возможности, которые есть уже сейчас.

Можно выбрать одно поле:

// DQL: SELECT e.email FROM ...
Spec::select('email')
// or
Spec::select(Spec::field('email'))

Можно добавить одно поле к выборке:

// DQL: SELECT e, u.email FROM ...
Spec::addSelect(Spec::field('email', $dqlAlias))

Можно выбрать несколько полей:

// DQL: SELECT e.title, e.cover, u.name, u.avatar FROM ...
Spec::andX( Spec::select('title', 'cover'), Spec::addSelect(Spec::field('name', $dqlAlias), Spec::field('avatar', $dqlAlias))
)

Можно добавить сущность к возвращаемым значениям:

// DQL: SELECT e, u FROM ...
Spec::addSelect(Spec::selectEntity($dqlAlias))

Можно использовать псевдонимы для выбираемых полей:

// DQL: SELECT e.name AS author FROM ...
Spec::select(Spec::selectAs(Spec::field('name'), 'author'))

Можно добавлять скрытые поля в выборку:

// DQL: SELECT e, u.name AS HIDDEN author FROM ...
Spec::addSelect(Spec::selectHiddenAs(Spec::field('email', $dqlAlias), 'author')))

Можно использовать выражения, например для получения скидки на товар:

// DQL: SELECT (e.price_old is not null and e.price_current < e.price_old) AS discount FROM ...
Spec::select(Spec::selectAs( Spec::andX( Spec::isNotNull('price_old'), Spec::lt(Spec::field('price_current'), Spec::field('price_old')) ), 'discount'
))

Можно использовать псевдонимы в спецификациях:

// DQL: SELECT e.price_current AS price FROM ... WHERE price < :low_cost_limit
Spec::andX( Spec::select(Spec::selectAs('price_current', 'price')), Spec::lt(Spec::alias('price'), $low_cost_limit)
)

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

Если это вам интересно, напишите в комментариях или в личку. PS: я могу на примере разобрать использование спецификаций и показать преимущества и недостатки их использования.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть