* * @method Teamer|null find($id, $lockMode = null, $lockVersion = null) * @method Teamer|null findOneBy(array $criteria, array $orderBy = null) * @method Teamer[] findAll() * @method Teamer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class TeamerRepository extends ServiceEntityRepository { use QueryHelperTrait; public function __construct( ManagerRegistry $registry, private readonly string $teamerInactivePeriod, ) { parent::__construct($registry, Teamer::class); } public function getListQuery(TeamerFilterDto $filterDto): Query { return $this ->createListQueryBuilder($filterDto) ->addOrderBy('teamer.viewed', \SortDirection::Ascending) ->addOrderBy('teamer.lastName', \SortDirection::Ascending) ->getQuery() ; } /** * The unpaginated list behind the filter, projected down to what a mailing needs. * * Hydrating entities here is a trap: the list query fetch-joins collections that would * multiply every teamer into several rows, and a teamer drags in a user account that * Doctrine cannot proxy - it is the inverse side of the relation - plus an EAGER photo, * which together cost a round trip per row. Selecting the handful of fields the mailing * reads avoids all of it and keeps the query at exactly one. * * @return TeamerMailingRecipient[] */ public function getMailingRecipients(TeamerFilterDto $filterDto): array { $rows = $this ->createListQueryBuilder($filterDto) ->distinct() ->select( 'teamer.id AS id', 'teamer.firstName AS firstName', 'teamer.lastName AS lastName', 'teamer.communication.email AS email', 'teamer.deletedAt AS deletedAt', 'user.disabledAt AS disabledAt' ) // teamer.viewed says nothing about a mailing and a DISTINCT select cannot order // by a field it does not carry, so this list sorts by name alone ->addOrderBy('teamer.lastName', \SortDirection::Ascending) ->getQuery() ->getArrayResult() ; return array_map(TeamerMailingRecipient::fromRow(...), $rows); } private function createListQueryBuilder(TeamerFilterDto $filterDto): QueryBuilder { $qb = $this->createQueryBuilder('teamer'); $qb ->select('teamer', 'user', 'feedback') ->leftJoin('teamer.availabilities', 'availability') ->leftJoin('teamer.jobProfiles', 'job_profile') ->leftJoin('teamer.licenses', 'license') ->leftJoin('teamer.feedback', 'feedback') ->leftJoin('teamer.user', 'user') ; if (null !== $filterDto->getName()) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->like('teamer.firstName', ':name'), $qb->expr()->like('teamer.lastName', ':name') )) ->setParameter('name', '%'.$this->escapeLikeWildcards($filterDto->getName()).'%') ; } if (null !== $filterDto->getAvailableAt()) { $qb ->andWhere($qb->expr()->andX( $qb->expr()->lte('availability.dateFrom', ':availableAt'), $qb->expr()->gte('availability.dateTo', ':availableAt') )) ->setParameter('availableAt', $filterDto->getAvailableAt()) ; } if (0 < count($filterDto->getJobProfiles())) { $qb ->andWhere($qb->expr()->in('job_profile', ':jobProfiles')) ->setParameter('jobProfiles', $filterDto->getJobProfiles()) ; } if ('' !== $filterDto->getPickupId()) { $qb ->andWhere($qb->expr()->like('teamer.pickups', ':pickup')) ->setParameter('pickup', '%"'.$this->escapeLikeWildcards($filterDto->getPickupId()).'"%') ; } if (true === $filterDto->hasSkiLicense()) { $qb ->andWhere($qb->expr()->eq('license.type', ':licenseSki')) ->setParameter('licenseSki', License::TYPE_SKI) ; } if (true === $filterDto->hasSnowboardLicense()) { $qb ->andWhere($qb->expr()->eq('license.type', ':licenseSnowboard')) ->setParameter('licenseSnowboard', License::TYPE_SNOWBOARD) ; } if (true === $filterDto->hasNoTrainings()) { $qb ->leftJoin('teamer.trainingAttendances', 'training_attendance') ->andWhere($qb->expr()->isNull('training_attendance')) ; } if (true === $filterDto->hasDriverLicenseVerified()) { $qb ->andWhere($qb->expr()->eq('teamer.driverLicenseVerified', ':driverLicenseVerified')) ->setParameter('driverLicenseVerified', true) ; } if (true === $filterDto->isAllSeason()) { $qb ->andWhere($qb->expr()->eq('teamer.allSeason', ':allSeason')) ->setParameter('allSeason', true) ; } if (false === $filterDto->isIncludeDeleted()) { $qb->andWhere($qb->expr()->isNull('teamer.deletedAt')); } // a deleted teamer never logs in again and would always count as inactive, so // asking for them has to lift this filter - otherwise they could not be found // again to be restored if (false === $filterDto->isIncludeInactive() && false === $filterDto->isIncludeDeleted()) { $qb ->andWhere($qb->expr()->gte('user.lastLoginAt', ':lastLoginThreshold')) ->setParameter('lastLoginThreshold', new \DateTimeImmutable($this->teamerInactivePeriod)) ; } return $qb; } /** * @param bool $allSeasonOnly restricts the suggestions to all-season teamers, so a season * assignment cannot be created for someone who works short term */ private function createAutocompletionQueryBuilder(string $search, bool $allSeasonOnly): QueryBuilder { $qb = $this->createQueryBuilder('teamer'); $qb ->where($qb->expr()->orX( $qb->expr()->like('teamer.lastName', ':search'), $qb->expr()->like('teamer.firstName', ':search'), )) ->andWhere($qb->expr()->isNull('teamer.deletedAt')) ->orderBy('teamer.lastName', \SortDirection::Ascending) ->addOrderBy('teamer.firstName', \SortDirection::Ascending) ->setParameter('search', '%'.$this->escapeLikeWildcards($search).'%') ; if (true === $allSeasonOnly) { $qb ->andWhere($qb->expr()->eq('teamer.allSeason', ':allSeason')) ->setParameter('allSeason', true) ; } return $qb; } public function getAutocompletionData(string $search, bool $allSeasonOnly = false): array { $teamers = $this ->createAutocompletionQueryBuilder($search, $allSeasonOnly) ->getQuery() ->getResult() ; $data = [ [ 'value' => '', 'text' => '...', ], ]; foreach ($teamers as $teamer) { $data[] = [ 'value' => $teamer->getId(), 'text' => (string) $teamer, ]; } return $data; } }