listingHandler = $listingHandler; $this->entityManager = $entityManager; } /** * @param array $data * @return Listing\ListingDefinition */ private function parseData($data): Listing\ListingDefinition { // check if sort order was specified or default to "desc" $data['sort_order'] = ($data['sort_order'] ?? null) === 'asc' ? 'asc' : 'desc'; // sanitize sort by $sortableColumns = ['email', 'status', 'created_at']; $sortBy = (!empty($data['sort_by']) && in_array($data['sort_by'], $sortableColumns, true)) ? $data['sort_by'] : ''; if (empty($sortBy)) { $sortBy = 'created_at'; } $data['sort_by'] = $sortBy; return $this->listingHandler->getListingDefinition($data); } /** * @param array $data * * @return array{ * count: int, * filters: array{}, * groups: array{}, * items: array> * } */ public function get($data = []): array { $definition = $this->parseData($data); $countQuery = $this->getBouncesQuery($definition, true); if ($countQuery) { $query = 'SELECT COUNT(*) as cnt FROM ( ' . $countQuery . ' ) t '; /** @var int $result */ $result = $this->entityManager->getConnection()->executeQuery($query)->fetchOne(); $count = intval($result); $query = $this->getBouncesQuery($definition); $query .= " ORDER BY {$definition->getSortBy()} {$definition->getSortOrder()} LIMIT :limit OFFSET :offset "; $items = $this ->entityManager ->getConnection() ->executeQuery($query, [ 'limit' => $definition->getLimit(), 'offset' => $definition->getOffset(), ], [ 'limit' => ParameterType::INTEGER, 'offset' => ParameterType::INTEGER, ]) ->fetchAllAssociative(); } else { $count = 0; $items = []; } return [ 'count' => $count, 'filters' => [], 'groups' => [], 'items' => $items, ]; } private function getBouncesQuery(Listing\ListingDefinition $definition, bool $count = false): ?string { $searchConstraint = ''; $newsletterId = intval($definition->getParameters()['id']); $subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $bouncesTable = $this->entityManager->getClassMetadata(StatisticsBounceEntity::class)->getTableName(); $searchString = $definition->getSearch(); if (is_string($searchString) && !empty($searchString)) { $searchConstraint = $this->getSearchConstraint($searchString); if ($searchConstraint === null) { // Nothing was found by search return null; } } $fields = [ 'bounces.id', 'bounces.created_at', 'bounces.subscriber_id', "'" . self::STATUS_BOUNCED . "' as status", 'subscribers.email', 'subscribers.first_name', 'subscribers.last_name', ]; return 'SELECT ' . self::getColumnList($fields, $count) . ' ' . 'FROM ' . $bouncesTable . ' bounces ' . 'LEFT JOIN ' . $subscriberTable . ' subscribers ON subscribers.id = bounces.subscriber_id ' . "WHERE bounces.newsletter_id = '" . $newsletterId . "' " . $searchConstraint; } private function getSearchConstraint(string $searchString): ?string { // Search recipients $search = trim($searchString); $search = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $search); // escape for 'LIKE' $qb = $this->entityManager->getConnection()->createQueryBuilder(); $qb->addSelect('id') ->from($this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName()) ->orWhere($qb->expr()->like('email', ':search')) ->orWhere($qb->expr()->like('first_name', ':search')) ->orWhere($qb->expr()->like('last_name', ':search')) ->setParameter('search', '%' . $search . '%'); $result = $qb->execute(); if (!$result instanceof Result) { // for PHPStan, it doesn't know execute always returns Statement for SELECT queries throw new \Exception('$statement must be an instance of Statement'); } $subscriberIds = $result->fetchAll(); $subscriberIds = array_column($subscriberIds, 'id'); if (empty($subscriberIds)) { return null; } return sprintf( ' AND subscribers.id IN (%s) ', join(',', array_map('intval', $subscriberIds)) ); } /** * @param array $fields * @param bool $count * * @return string */ private static function getColumnList(array $fields, bool $count = false): string { // Select ID field only for counting return $count ? (string)reset($fields) : join(', ', $fields); } }