
Кратко о спецификациях:
Спецификация — это шаблон проектирования, с помощью которого можно отразить правила бизнес-логики в виде цепочки объектов, связанных операциями булевой логики. Спецификации позволяют избавится от повторяющихся, однотипных методов в репозитории и от дублирования бизнес-логики.
На сегодня существует два (если знаете другие проекты, напишите пожалуйста в комментариях) успешных и популярных проекта на 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— эквивалент SQLIS NULLisNotNull— эквивалент SQLIS NOT NULLin— эквивалентIN ()notIn— эквивалентNOT IN ()eq— проверка на равенство=neq— проверка на неравенство!=lt— меньше чем<lte— меньше или равно<=gt— больше чем>gte— больше или равно>=like— эквивалент SQLLIKEinstanceOfX— эквивалент DQLINSTANCE 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.
joinleftJoininnerJoinlimitoffsetorderBygroupByhaving
Хочу отдельно отметить модификатор slice. Он объединяет в себе функции limit и offset и сам высчитывает offset исходя из размера слайса и его порядкового номера. В реализации этого модификатора мы разошлись во мнениях с автором проекта. Создавая модификатор я преследовал цель упрощения конфигурирования спецификаций при пагинации. В этом контексте первая страница с порядковым номером 1 должна была быть эквивалентна первому слайсу с порядковым номером 1. Но автор проекта посчитал правильным начинать отсчёт в стиле программирования, то есть с 0. Пэтому стоит помнить, что если вам нужен первый слайс, вам необходимо указывать 0 в качестве порядкового номера.
Модификаторы результата
Модификаторы результата существуют немного отдельно от спецификаций. Они применяются к Doctrine Query. Следующие модификаторы управляют гидрацией данных (Query::setHydrationMode()):
asArrayasSingleScalarasScalar
Модификатор 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: я могу на примере разобрать использование спецификаций и показать преимущества и недостатки их использования. Если это вам интересно, напишите в комментариях или в личку.