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

от автора

Happyr Doctrine Specification

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

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

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

Кратко о Doctrine Specification

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

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

Спецификации применяются через 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

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

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

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

  • asArray
  • asSingleScalar
  • asScalar

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

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

Встроенные спецификации

В Happyr Doctrine-Specification для спецификаций выделен отдельный интерфейс который объединяет в себе фильтр и модификатор запроса. Единственная предустановленная спецификация это countOf позволяющая получить количество сущностей соответствующее спецификации. Для создания собственных спецификаций принято расширять абстрактный класс 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, но обещают исправить).

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

В новом релизе 1.0.0 добавились модификаторы запроса select и addSelect. 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: я могу на примере разобрать использование спецификаций и показать преимущества и недостатки их использования. Если это вам интересно, напишите в комментариях или в личку.


ссылка на оригинал статьи https://habr.com/ru/post/455030/


Комментарии

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

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