<?php
namespace Products\NotificationsBundle\Service;
use App\Doctrine\Repository\SearchableRepositoryInterface;
use App\Model\Query\ConditionQuery\ConditionQuery;
use App\Model\Searching\AbstractSearch;
use Cms\CoreBundle\Doctrine\Hydrators\SingleColumnHydrator;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Products\NotificationsBundle\Entity\AbstractList;
use Products\NotificationsBundle\Entity\AbstractNotification;
use Products\NotificationsBundle\Entity\AbstractRecipient;
use Products\NotificationsBundle\Entity\Lists\ConditionList;
use Products\NotificationsBundle\Entity\Notifications\Invocation;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
use Products\NotificationsBundle\Entity\Student;
use Products\NotificationsBundle\Form\Forms\Profiles\ProfileBuilderSearchForm;
use Products\NotificationsBundle\Form\Forms\Profiles\ProfileSearchForm;
use Products\NotificationsBundle\Form\Forms\Students\StudentBuilderSearchForm;
use Products\NotificationsBundle\Form\Forms\Students\StudentSearchForm;
use Products\NotificationsBundle\Model\Searching\ProfileSearch;
use Products\NotificationsBundle\Model\Searching\StudentSearch;
use Products\NotificationsBundle\Service\ChannelHandlers\AbstractChannelHandler;
use Products\NotificationsBundle\Util\ListBuilder\AbstractListBuilder;
use Products\NotificationsBundle\Util\Preferences;
class ListItemProvider
{
public const ITEM__TYPE_PROFILES = 'profiles';
public const ITEM__TYPE_STUDENTS = 'students';
public const SEARCH__CLASS_MAP = [
self::ITEM__TYPE_PROFILES => ProfileSearch::class,
self::ITEM__TYPE_STUDENTS => StudentSearch::class,
];
public const SEARCH__FORM_CLASS_MAP = [
self::ITEM__TYPE_PROFILES => ProfileSearchForm::class,
self::ITEM__TYPE_STUDENTS => StudentSearchForm::class,
];
public const BUILDER__SEARCH_FORM_CLASS_MAP = [
self::ITEM__TYPE_PROFILES => ProfileBuilderSearchForm::class,
self::ITEM__TYPE_STUDENTS => StudentBuilderSearchForm::class,
];
public const ITEM__TYPE_CLASS_MAP = [
self::ITEM__TYPE_PROFILES => Profile::class,
self::ITEM__TYPE_STUDENTS => Student::class,
];
/**
* @var EntityManager
*/
private EntityManager $em;
/**
* @var ListBuilderService
*/
private ListBuilderService $listBuilderService;
/**
* @param EntityManager $em
* @param ListBuilderService $listBuilderService
*/
public function __construct(EntityManager $em, ListBuilderService $listBuilderService)
{
$this->em = $em;
$this->listBuilderService = $listBuilderService;
}
/**
* @param AbstractList $list
* @param AbstractSearch $search
* @param int|null $limit
* @param int|null $offset
* @return Paginator
*/
public function getItems(
AbstractList $list,
AbstractSearch $search,
?int $limit = null,
?int $offset = null
): Paginator
{
// get the repository, should be a searchable one
$repo = $this->em->getRepository($list->getEntityClass());
if ( ! $repo instanceof SearchableRepositoryInterface) {
throw new \RuntimeException();
}
return $repo->findBySearch(
$search,
$limit,
$offset,
$this->listBuilderService->build(
$list,
$list->getEntityClass(),
),
);
}
/**
* @param AbstractList $list
* @return string
*/
public function getItemType(AbstractList $list): string
{
if ($list instanceof ConditionList &&
($conditionQuery = $list->getConditionQuery()) instanceof ConditionQuery &&
$conditionQuery->getEntity(true) === ConditionQuery::STUDENT_ENTITY) {
return self::ITEM__TYPE_STUDENTS;
}
return self::ITEM__TYPE_PROFILES;
}
/**
* Counts all the total items in the database for the "entity" class of a given list.
*
* @param AbstractList $list
* @return int
*/
public function count(AbstractList $list): int
{
return $this->em->getRepository($list->getEntityClass())->count([]);
}
/**
* Counts only the items in the given list, based on that lists "entity" class.
*
* @param array<AbstractList>|AbstractList $lists
* @return int
*/
public function countByLists($lists): int
{
if ( ! $lists) {
return 0;
}
return $this->listBuilderService
->count($lists)
->getQuery()
->getSingleScalarResult();
}
/**
* @param AbstractNotification $notification
* @param int $preferences
* @param string|null $recipientType
* @return array<int>|array<array<string,int>>
*/
public function identifyByNotification(
AbstractNotification $notification,
int $preferences,
?string $recipientType = null
): array
{
// create the base query builder for identification based on this notification's lists
$qb = $this->identifyByLists(
$notification->getListsAsArray(),
);
// apply the main preferences to the query
$qb
->andWhere(
sprintf(
'BIT_AND(%s.%s, :preferences) > 0',
AbstractListBuilder::ENTITIES__PROFILE_CONTACTS,
// urgent setting determines if primary or secondary preferences are used
$notification->isUrgent() ? 'primaryPreferences' : 'secondaryPreferences',
)
)
->setParameter('preferences', $preferences);
// if we are using a phone type, we need to do some extra work...
if (($preferences & (Preferences::PREFERENCES__SMS | Preferences::PREFERENCES__VOICE))) {
$qb
->andWhere(AbstractListBuilder::ENTITIES__RECIPIENTS.'.method IN (:methods)')
->setParameter(
'methods',
array_values(array_filter(array_unique(array_merge(
($preferences & Preferences::PREFERENCES__SMS) ? PhoneRecipient::SMS_METHODS : [],
($preferences & Preferences::PREFERENCES__VOICE) ? PhoneRecipient::VOICE_METHODS : [],
)))),
);
}
// track hydration
$hydrator = SingleColumnHydrator::HYDRATOR;
// handle invocations
if ($notification instanceof Invocation) {
$itemEntity = AbstractListBuilder::ENTITIES__PROFILES;
$itemId = AbstractChannelHandler::ITEMS__PROFILE;
if ($notification->getConditionQuery()->getEntity(true) === ConditionQuery::STUDENT_ENTITY) {
$itemEntity = AbstractListBuilder::ENTITIES__STUDENTS;
$itemId = AbstractChannelHandler::ITEMS__STUDENT;
}
// modify the query
$qb
->select(
sprintf(
'%s.id AS %s, %s.id AS %s',
AbstractListBuilder::ENTITIES__RECIPIENTS,
AbstractChannelHandler::ITEMS__TARGET,
$itemEntity,
$itemId,
)
)
->addGroupBy(AbstractListBuilder::ENTITIES__RECIPIENTS.'.id')
->addGroupBy($itemEntity.'.id');
if ($itemEntity === AbstractListBuilder::ENTITIES__STUDENTS) {
$qb
->leftJoin(
AbstractListBuilder::ENTITIES__PROFILES . '.contacts',
AbstractListBuilder::ENTITIES__PROFILE_CONTACTS,
)
->leftJoin(
AbstractListBuilder::ENTITIES__PROFILE_CONTACTS . '.recipient',
AbstractListBuilder::ENTITIES__RECIPIENTS,
);
}
// need to use a different hydrator as we are returning an array of information
$hydrator = AbstractQuery::HYDRATE_ARRAY;
}
// need to replace the join with the proper Recipient class
if ($recipientType !== null) {
$joinDqlPart = $qb->getDQLPart('join');
$qb->resetDQLPart('join');
$rootAlias = $qb->getRootAliases()[0];
/** @var Join $join */
foreach ($joinDqlPart[$rootAlias] as $join) {
if ($join->getAlias() === AbstractListBuilder::ENTITIES__RECIPIENTS) {
$join = new Join(
$join->getJoinType(),
AbstractRecipient::DISCRS[$recipientType],
$join->getAlias(),
Join::WITH,
AbstractListBuilder::ENTITIES__PROFILE_CONTACTS . '.recipient = ' . AbstractListBuilder::ENTITIES__RECIPIENTS . '.id'
);
}
$qb->add('join', [$rootAlias => $join], true);
}
}
// perform the query
return $qb
->getQuery()
->getResult($hydrator);
}
/**
* @param AbstractList|array<AbstractList> $lists
* @return QueryBuilder
*/
protected function identifyByLists($lists): QueryBuilder
{
// normalize lists
$lists = !is_array($lists) ? [$lists] : $lists;
// create the basic query builder, will need to apply preference logic to it later
$qb = $this->listBuilderService->identify($lists);
// ensure the recipients are active and their contacts enabled
$qb
->andWhere(AbstractListBuilder::ENTITIES__RECIPIENTS.'.active = :active')
->setParameter('active', true)
->andWhere(AbstractListBuilder::ENTITIES__PROFILE_CONTACTS.'.enabled = :enabled')
->setParameter('enabled', true);
return $qb
->resetDQLParts(['select', 'orderBy'])
->select(
sprintf(
'DISTINCT(%s.id)',
AbstractListBuilder::ENTITIES__RECIPIENTS
)
);
}
}