symfony-doge/veslo

View on GitHub
src/AnthillBundle/Entity/Repository/VacancyRepository.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/*
 * This file is part of the Veslo project <https://github.com/symfony-doge/veslo>.
 *
 * (C) 2019 Pavel Petrov <itnelo@gmail.com>.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license https://opensource.org/licenses/GPL-3.0 GPL-3.0
 */

declare(strict_types=1);

namespace Veslo\AnthillBundle\Entity\Repository;

use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Cache;
use Doctrine\ORM\QueryBuilder;
use Knp\Component\Pager\Pagination\AbstractPagination;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Veslo\AnthillBundle\Entity\Vacancy;
use Veslo\AnthillBundle\Entity\Vacancy\Category;
use Veslo\AppBundle\Dto\Paginator\CriteriaDto as PaginationCriteria;
use Veslo\AppBundle\Entity\Repository\BaseEntityRepository;
use Veslo\AppBundle\Entity\Repository\PaginateableInterface;

/**
 * Vacancy repository
 */
class VacancyRepository extends BaseEntityRepository implements PaginateableInterface
{
    /**
     * A hint for the pagination building process to include a specific category match statement
     *
     * Usage example:
     * ```
     * $paginationCriteria->addHint(VacancyRepository::PAGINATION_HINT_CATEGORY, $category);
     * ```
     *
     * @const string
     */
    public const PAGINATION_HINT_CATEGORY = 'category';

    /**
     * A hint for the pagination building process to include a sync date after filter statement
     *
     * @const string
     */
    public const PAGINATION_HINT_SYNC_DATE_AFTER = 'synchronization_date_after';

    /**
     * A hint for the pagination building process to include a sync date before filter statement
     *
     * @const string
     */
    public const PAGINATION_HINT_SYNC_DATE_BEFORE = 'synchronization_date_before';

    /**
     * Modifies vacancy search query to provide data in small bunches
     *
     * @var PaginatorInterface
     */
    private $paginator;

    /**
     * Options for vacancy repository, ex. page cache time
     *
     * @var array
     */
    private $options;

    /**
     * {@inheritdoc}
     */
    public function setPaginator(PaginatorInterface $paginator): void
    {
        $this->paginator = $paginator;
    }

    /**
     * Sets options for vacancy repository
     *
     * @param array $options Options for vacancy repository, ex. page cache time
     *
     * @return void
     */
    public function setOptions(array $options): void
    {
        $optionsResolver = new OptionsResolver();
        $optionsResolver->setDefaults(
            [
                'cache_result_namespace' => md5(__CLASS__),
                'cache_result_lifetime'  => 300,
            ]
        );

        $this->options = $optionsResolver->resolve($options);
    }

    /**
     * Returns vacancy by specified SEO-friendly identifier
     *
     * @param string $slug SEO-friendly vacancy identifier
     *
     * @return Vacancy|null
     */
    public function findBySlug(string $slug): ?Vacancy
    {
        $criteria = new Criteria();
        $criteria->andWhere($criteria->expr()->eq('e.slug', $slug));

        return $this->getQuery($criteria)->getOneOrNullResult();
    }

    /**
     * Returns vacancy by roadmap name and identifier on external job website
     *
     * @param string $roadmapName        Roadmap name
     * @param string $externalIdentifier Identifier on external job website
     *
     * @return Vacancy|null
     */
    public function findByRoadmapNameAndExternalIdentifier(string $roadmapName, string $externalIdentifier): ?Vacancy
    {
        $criteria = new Criteria();
        $criteria
            ->andWhere($criteria->expr()->eq('e.roadmapName', $roadmapName))
            ->andWhere($criteria->expr()->eq('e.externalIdentifier', $externalIdentifier))
        ;

        $query = $this->getQuery($criteria);

        return $query->getOneOrNullResult();
    }

    /**
     * {@inheritdoc}
     *
     * Prevents exposition of database layer details to other application layers
     * All doctrine-related things (ex. criteria building) remains encapsulated
     */
    public function getPagination(PaginationCriteria $criteria): AbstractPagination
    {
        $queryBuilder = $this->createQueryBuilder('e');
        $queryBuilder
            // fetch joins for caching.
            ->innerJoin('e.company', 'c')
            ->addSelect('c')
            // inner join for company is required; due to fixtures loading logic there are some cases
            // when a deletion date can be set in company and not present in related vacancies at the same time.
            // it leads to inconsistent state in the test environment; normally, a soft delete logic should be
            // properly applied for all relations at once.
            ->leftJoin('e.categories', 'ct')
            ->addSelect('ct')
            ->orderBy('e.id', Criteria::DESC)
        ;

        $queryBuilder = $this->applyPaginationHints($queryBuilder, $criteria);

        list($page, $limit, $options) = [$criteria->getPage(), $criteria->getLimit(), $criteria->getOptions()];

        $query = $queryBuilder->getQuery();
        $query
            ->setCacheable(true)
            ->setCacheMode(Cache::MODE_NORMAL)
        ;

        /** @var AbstractPagination $pagination */
        $pagination = $this->paginator->paginate($query, $page, $limit, $options);

        return $pagination;
    }

    /**
     * Returns a query builder instance modified according to the pagination hints, provided by criteria
     *
     * @param QueryBuilder       $queryBuilder A query builder instance
     * @param PaginationCriteria $criteria     Pagination criteria
     *
     * @return QueryBuilder
     */
    private function applyPaginationHints(QueryBuilder $queryBuilder, PaginationCriteria $criteria): QueryBuilder
    {
        $paginationHints = $criteria->getHints();

        // Hint: category.
        if (array_key_exists(self::PAGINATION_HINT_CATEGORY, $paginationHints)) {
            /** @var Category $category */
            $category = $paginationHints[self::PAGINATION_HINT_CATEGORY];

            $queryBuilder
                ->andWhere($queryBuilder->expr()->isMemberOf(':category', 'e.categories'))
                ->setParameter('category', $category)
            ;
        }

        // Hint: sync date before.
        if (array_key_exists(self::PAGINATION_HINT_SYNC_DATE_BEFORE, $paginationHints)) {
            $syncDateUpperBound = $paginationHints[self::PAGINATION_HINT_SYNC_DATE_BEFORE];

            $queryBuilder
                ->andWhere($queryBuilder->expr()->lt('e.synchronizationDate', ':syncDateUpperBound'))
                ->setParameter('syncDateUpperBound', $syncDateUpperBound)
            ;
        }

        // Hint: sync date after.
        if (array_key_exists(self::PAGINATION_HINT_SYNC_DATE_AFTER, $paginationHints)) {
            $syncDateLowerBound = $paginationHints[self::PAGINATION_HINT_SYNC_DATE_AFTER];

            $queryBuilder
                ->andWhere($queryBuilder->expr()->gte('e.synchronizationDate', ':syncDateLowerBound'))
                ->setParameter('syncDateLowerBound', $syncDateLowerBound)
            ;
        }

        return $queryBuilder;
    }
}