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;
    }
 

Greetapp – Specification Pattern Example

https://github.com/JonnyD/greetapp-api-java

Small example of the Specification pattern:

/**
     * GET  /greets-by-user/:groupId : get all the greets by group.
     *
     * @return the ResponseEntity with status 200 (OK) and the list of greets in body
     */
    @GetMapping("/greets-by-group/{groupId}")
    @Timed
    public Object getAllGreetsByGroup(@PathVariable Long groupId) {
        log.debug("REST request to get all Greets by user");
        List<Greet> greets = greetService.getByGang(groupId);

        Optional<Gang> optionalGang = this.gangService.getById(groupId);
        Gang gang = optionalGang.get();

        CanViewGang canViewGang = new CanViewGang(this.userService);
        if (!canViewGang.isSatisfiedBy(gang)) {
            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
        }

        CanViewGreetsByGang canViewGreetsByGang = new CanViewGreetsByGang(this.userRepository);

        List<Greet> greetsToShow = new ArrayList<Greet>();
        for (Greet greet : greets) {
            if (canViewGreetsByGang.isSatisfiedBy(greet)) {
                greetsToShow.add(greet);
            }
        }

        return greetsToShow;
    }
public class CanViewGreetsByGang extends AbstractSpecification<Greet> {
    private User loggedInUser;

    public CanViewGreetsByGang(UserRepository userRepository) {
        Optional<String> login = SecurityUtils.getCurrentUserLogin();
        this.loggedInUser = userRepository.findOneByLogin(login.get()).get();
    }

    public boolean isSatisfiedBy(Greet greet) {
        IsGangPublic isGangPublic = new IsGangPublic();
        IsHostOfGreet isHostOfGreet = new IsHostOfGreet(this.loggedInUser);
        IsMemberOfGang isMemberOfGang = new IsMemberOfGang(this.loggedInUser);

        boolean isGangPublicOrIsMemberOfGangBoolean = isGangPublic
            .or(isMemberOfGang)
            .isSatisfiedBy(greet.getGang());

        boolean isHostOfGreetBoolean = isHostOfGreet
            .isSatisfiedBy(greet);

        return isGangPublicOrIsMemberOfGangBoolean || isHostOfGreetBoolean;
    }
}
 

Introducing Peggy

PHP wrapper for 80legs.com API https://github.com/JonnyD/Peggy

$peggy = new Peggy\Client('<your api key>');

Crawls

// create crawl
$request = $peggy->crawl()->createCrawlRequest($crawlName, $appName, $urllist, $maxDepth, $maxUrls);
$peggy->crawl()->create($request);

// get crawl
$crawl = $peggy->crawl()->get($crawlName);
echo $crawl->getName();

// cancel crawl
$peggy->crawl()->cancel($crawlName);

// get all crawls
$allCrawls = $peggy->crawl()->all();
foreach ($allCrawls as $crawl) {
   echo $crawl->getName();
}

Results

// get result
$result = $peggy->result()->get($crawlName);
$urls = $result->getUrls(); // Returns the results of the crawl specified by CRAWL_NAME. This will return a 404 if no results have been posted. Example Url: "http://s3.amazonaws.com/results1"

Apps

// upload app
$request = $peggy->app()->createAppRequest($name, $filePath);
$peggy->app()->upload($request);

// get app
$app = $peggy->app()->get($appName); // this API is broken on 80legs.com

// remove app
$peggy->app()->remove($appName);

// get all apps
$allApps = $peggy->app()->all();
foreach ($allApps as $app) {
    echo $app->getName();
}

Url Lists

// create url list
$request = $peggy->urllist()->createUrllistRequest($name, $filePath);
$peggy->urllist()->upload($request);

// get url list
$urllist = $peggy->urllist()->get($name);
echo $urllist->getName();

// remove url list
$peggy->urllist->remove($name);

// get all url lists
$allUrllists = $peggy->urllist()->all();
foreach ($allUrllists as $urllist) {
    echo $urllist->getName();
}

User

// get me
$me = $peggy->user()->me();

// get user
$user = $peggy->user()->get($token);