The Criteria Pattern

You’ve probably come across a Repository like the following:

class DocumentaryRepository
{
    public function findFeaturedDocumentaries() {
        //sql..
    }

    public function findPublishedDocumentariesInCategoryOrdededByCreated(Category $category) {
        //sql
    }

    public function findMostPopularDocumentaries() {
        //sql
    }

    public function findMostDiscussedDocumentaries() {
        //sql
    }

    public function findMostWatchlistedDocumentaries() {
        //sql
    }

    public function findRandomDocumentariesInCategory(Category $category) {
        //sql
    }

    public function findPublishedDocumentariesOrderedByCreated() {
        //sql
    }

    public function getLatestDocumentariesOrderedByCreated() {
        //sql
    }
}

This eventually can become cumbersome.

Another way to look at this is using the Criteria Pattern where you just have one function in the repository to handle the querying of the database.

First lets build our DocumentaryCriteria class:

<?php

namespace App\Criteria;

use App\Entity\Category;
use App\Entity\User;
use App\Entity\VideoSource;

class DocumentaryCriteria
{
    /**
     * @var bool
     */
    private $featured;

    /**
     * @var string
     */
    private $status;

    /**
     * @var Category
     */
    private $category;

    /**
     * @var VideoSource
     */
    private $videoSource;

    /**
     * @var int
     */
    private $year;

    /**
     * @var string
     */
    private $duration;

    /**
     * @var User
     */
    private $addedBy;

    /**
     * @var array
     */
    private $sort;

    /**
     * @var int
     */
    private $limit;

    //getter and setters
}

Another class required is DocumentaryOrderBy

<?php

namespace App\Enum;

class DocumentaryOrderBy
{
    const CREATED_AT = "createdAt";
    const UPDATED_AT = "updatedAt";
    const VIEWS = "views";
    const COMMENT_COUNT = "commentCount";
    const WATCHLIST_COUNT = "watchlistCount";
    const YEAR = "year";
}

and Order

<?php

namespace App\Enum;

class Order
{
    const ASC = "ASC";
    const DESC = "DESC";
}

Now lets look at the DocumentaryRepository and add the criteria function

/**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function findDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        $em = $this->getEntityManager();
        $qb = $em->createQueryBuilder();

        $qb->select('documentary')
            ->from('App\Entity\Documentary', 'documentary');

        if ($criteria->isFeatured() != null) {
            $qb->andWhere('documentary.featured = :featured')
                ->setParameter('featured', $criteria->isFeatured());
        }

        if ($criteria->getStatus()) {
            $qb->andWhere('documentary.status = :status')
                ->setParameter('status', $criteria->getStatus());
        }

        if ($criteria->getCategory()) {
            $qb->andWhere('documentary.category = :category')
                ->setParameter('category', $criteria->getCategory());
        }

        if ($criteria->getVideoSource()) {
            $qb->andWhere('documentary.videoSource = :videoSource')
                ->setParameter('videoSource', $criteria->getVideoSource());
        }

        if ($criteria->getAddedBy()) {
            $qb->andWhere('documentary.addedBy = :addedBy')
                ->setParameter('addedBy', $criteria->getAddedBy());
        }
        
        if ($criteria->getYear()) {
            $qb->andWhere('documentary.year = :year')
                ->setParameter('year', $criteria->getYear());
        }

        if ($criteria->getSort()) {
            foreach ($criteria->getSort() as $column => $direction) {
                $qb->addOrderBy($qb->getRootAliases()[0] . '.' . $column, $direction);
            }
        }

        if ($criteria->getLimit()) {
            $qb->setMaxResults($criteria->getLimit());
        }

        return $qb;
    }

We’re naming the function findDocumentariesByCriteriaQueryBuilder for a reason (which will come in handy later when we implement pagination).

Lets add another function:


    /**
     * @param DocumentaryCriteria $criteria
     * @return ArrayCollection|Documentary[]
     */
    public function findDocumentariesByCriteria(DocumentaryCriteria $criteria)
    {
        $qb = $this->findDocumentariesByCriteriaQueryBuilder($criteria);

        $query = $qb->getQuery();
        $result = $query->getResult();

        return $result;
    }

Now we can add DocumentaryService which will host more finely grained functions but it’s much simpler with DocumentaryCriteria:


    /**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function getDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        return $this->documentaryRepository->findDocumentariesByCriteriaQueryBuilder($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getFeaturedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setFeatured(true);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);

        $documentaries = $this->documentaryRepository->findDocumentariesByCriteria($criteria);
        shuffle($documentaries);
        return $documentaries;
    }

    /**
     * @param Category $category
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentariesInCategory(Category $category)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setCategory($category);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostPopularDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::VIEWS => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostDiscussedDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::COMMENT_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostWatchlistedDocumentaries(int $limit) : array
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::WATCHLIST_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getLatestDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setLimit($limit);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

Now you can see how the criteria can make your life easier and we can easily add pagination with the QueryBuilder and Pagerfanta.

 /**
     * @FOSRest\Get("/documentary", name="get_documentary_list", options={ "method_prefix" = false })
     *
     * @param Request $request
     * @throws \Doctrine\ORM\ORMException
     */
    public function listAction(Request $request)
    {
        $page = $request->query->get('page', 1);

        $criteria = new DocumentaryCriteria();

        $isRoleAdmin = $this->isGranted('ROLE_ADMIN');

        if ($isRoleAdmin) {
            $videoSourceId = $request->query->get('videoSource');
            if (isset($videoSourceId)) {
                $videoSource = $this->videoSourceService->getVideoSourceById($videoSourceId);
                $criteria->setVideoSource($videoSource);
            }

            $status = $request->query->get('status');
            if (isset($status)) {
                $criteria->setStatus($status);
            }

            $featured = $request->query->get('featured');
            if (isset($featured)) {
                $featured = $featured === 'true' ? true: false;
                $criteria->setFeatured($featured);
            }
        }

        if (!$isRoleAdmin) {
            $criteria->setStatus(DocumentaryStatus::PUBLISH);
        }

        $categorySlug = $request->query->get('category');
        if (isset($categorySlug)) {
            $category = $this->categoryService->getCategoryBySlug($categorySlug);
            $criteria->setCategory($category);
        }

        $year = $request->query->get('year');
        if (isset($year)) {
            $criteria->setYear($year);
        }

        $duration = $request->query->get('duration');
        if (isset($duration)) {
            $criteria->setDuration($duration);
        }

        $addedBy = $request->query->get('addedBy');
        if (isset($addedBy)) {
            $user = $this->userService->getUserByUsername($addedBy);
            $criteria->setAddedBy($user);
        }

        $sort = $request->query->get('sort');
        if (isset($sort)) {
            $exploded = explode("-", $sort);
            $sort = [$exploded[0] => $exploded[1]];
            $criteria->setSort($sort);
        } else {
            $criteria->setSort([
                DocumentaryOrderBy::CREATED_AT => Order::DESC
            ]);
        }

        $qb = $this->documentaryService->getDocumentariesByCriteriaQueryBuilder($criteria);

        $adapter = new DoctrineORMAdapter($qb, false);
        $pagerfanta = new Pagerfanta($adapter);
        $pagerfanta->setMaxPerPage($amountPerPage);
        $pagerfanta->setCurrentPage($page);

        $items = (array) $pagerfanta->getCurrentPageResults();

        $serialized = [];
        foreach ($items as $item) {
            $serialized[] = $this->serializeDocumentary($item);
        }

        $data = [
            'items'             => $serialized,
            'count_results'     => $pagerfanta->getNbResults(),
            'current_page'      => $pagerfanta->getCurrentPage(),
            'number_of_pages'   => $pagerfanta->getNbPages(),
            'next'              => ($pagerfanta->hasNextPage()) ? $pagerfanta->getNextPage() : null,
            'prev'              => ($pagerfanta->hasPreviousPage()) ? $pagerfanta->getPreviousPage() : null,
            'paginate'          => $pagerfanta->haveToPaginate(),
        ];

        return new JsonResponse($data, 200, array('Access-Control-Allow-Origin'=> '*'));
    }

Feedback appreciated.

 

Modulus Examples

aka Fizzbuzz alternatives.

getColumnsForCategories(categories) {
    let categoriesCount = 0;
    for (var key in categories) {
      categoriesCount++;
    }

    let half = Math.floor(categoriesCount / 2);
    let remainder = categoriesCount % half;

    let categoriesLeftColumnLength = half + remainder;
    let categoriesRightColumnLength = half;

    let categoriesLeftColumn = new Set();
    for (let i = 0; i < categoriesLeftColumnLength; i++) {
      categoriesLeftColumn.add(categories[i]);
    }

    let categoriesRighttColumn = new Set();
    for (let i = categoriesLeftColumnLength; i < (categoriesLeftColumnLength + categoriesRightColumnLength); i++) {
      categoriesRighttColumn.add(categories[i]);
    }

    let categoriesColumns = new Map();
    categoriesColumns.set('left', categoriesLeftColumn);
    categoriesColumns.set('right', categoriesRighttColumn);

    return categoriesColumns;
   }
convertArrayOfDocumentariesToMap(documentaries, amountPerRow, amountTotal) {
    let cardDecks = new Map();

    let counter = 0;
    for (let i in documentaries) {
        if (+i === amountTotal) {
          break;
        }

        let documentary = documentaries[i];

        let cardDeck = cardDecks.get(counter);
        if (cardDeck === undefined || !cardDeck) {
          cardDeck = new Set();
        } 

        cardDeck.add(documentary);
        cardDecks.set(counter, cardDeck);

        if (+i != 0 && (+i + 1) % amountPerRow === 0) {
          counter++;
        }
    }

    return cardDecks;
   }
 

Parent & Children Activity

private function convertActivityToArray(array $activity)
    {
        $activityArray = array();

        $previousGroupNumber = null;
        /** @var Activity $activityItem */
        foreach ($activity as $activityItem) {
            $type = $activityItem->getType();
            $groupNumber = $activityItem->getGroupNumber();
            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $data = $activityItem->getData();
            $created = $activityItem->getCreatedAt();

            $activityArray[$groupNumber]['type'] = $type;
            $activityArray[$groupNumber]['created'] = $created;

            if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

        return $activityArray;
    }