aaronbullard/dogpile

View on GitHub
src/QueryBuilder.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

namespace Dogpile;

use Dogpile\Collections\Collection;
use Dogpile\Collections\ResourceCollection;
use Dogpile\Collections\RelationshipCollection;

class QueryBuilder
{
    const ROOT = "$";

    protected $manager;

    protected $relationships;

    protected $resources;

    protected $paths;

    protected $completedPaths = [];

    public function __construct(ResourceManager $manager)
    {
        $this->manager          = $manager;
        $this->relationships    = new RelationshipCollection();
        $this->resources        = new ResourceCollection();
        $this->paths            = new Collection();
    }

    public function relationships(): RelationshipCollection
    {
        return $this->relationships;
    }

    public function resources(): ResourceCollection
    {
        return $this->resources;
    }

    public function includes(): Collection
    {
        return $this->paths;
    }

    public function setRelationships(RelationshipCollection $relationships): QueryBuilder
    {
        $this->relationships->mergeRelationships($relationships);

        return $this;
    }

    public function include(string ...$paths): QueryBuilder
    {
        // remove duplicates
        // Sorting allows parents to go before children e.g. author, author.comments
        $this->paths = $this->paths->merge($paths)->unique()->sort()->values();
        
        return $this;
    }

    public function query(): ResourceCollection
    {
        $this->includes()->each(function($path){
            $this->resolve($path);
        });

        // At this point, all resources have been gathered
        // Let's return only what was requested PLUS the required
        // intermediate resources to provide full linkage as per
        // https://jsonapi.org/format/#fetching-includes

        // get identifiers for all necessary paths
        return Collection::wrap($this->completedPaths)
            ->map(function($path){
                return $this->relationships->identifiersFor($path);
            })
            ->flatten()
            // Map resource identifier to resource object
            ->map(function($identifier){
                return $this->resources->find($identifier->type(), $identifier->id());
            })
            // return back a collection of resources that were requested
            ->pipe(function($resources){
                return new ResourceCollection($resources->toArray());
            });
    }

    protected function resolve(string $path)
    {
        // If recursion brought us to the root, stop
        if(static::isRoot($path)){
            return;
        }

        // Has this path already been queried
        if(in_array($path, $this->completedPaths)){
            return;
        }

        // do we have the identifiers 
        if(false === $this->relationships->has($path)){
            // (inception...) Let's get resources from our parent e.g. if no ids for comments.author, go query comments
            $this->resolve(static::parent($path));
        }

        // We now have our identifiers, go get the resources
        $identifiers = $this->relationships->identifiersFor($path);

        $resources = $this->queryResources($identifiers);

        // update ResourceCollection
        $this->resources->add(...array_values($resources->all()));

        // need ALL Resources (not just ones queried) for this path
        // need to update the relationships collection with the child related identifiers
        $allResourcesInPath = $identifiers->map(function($ident){
                return $this->resources->find($ident->type(), $ident->id());
            })
            ->pipe(function($resources){ return new ResourceCollection($resources->toArray()); });

        // update RelationshipCollection with new child relationships for other queries
        $this->indexRelationships($path, $allResourcesInPath->relationships())->each(function($identifiers, $nestedRelationshipType){
            $this->relationships->add($nestedRelationshipType, ...$identifiers);
        });

        // Update completed paths for faster operation
        $this->completedPaths[] = $path;
    }

    /**
     * Query and return all resources which we don't already have in $this->resources
     *
     * @param string $path
     * @return ResourceCollection
     */
    protected function queryResources(Collection $identifiers): ResourceCollection
    {
            // filter out identifiers for resources we already have
        return $identifiers->filter(function($identifier){
                return false === $this->resources()->exists($identifier->type(), $identifier->id());
            })
            // group identifiers by their type for group querying
            ->groupBy(function($identifier){ return $identifier->type(); })
            // map Identifiers to Resources
            ->map(function($identifiers, $type){
                return $this->manager->repositoryFor($type)->findHavingIds(
                    $identifiers->map(function($identifier){ return $identifier->id(); })->toArray()
                );
            })
            // get rid of hash grouping by type, return list of resources
            ->flatten()
            ->pipe(function($collection){
                return new ResourceCollection($collection->toArray());
            });
    }

    /**
     * Prepend the current relationship path to the child relationships
     * 
     * e.g. comments => post.comments
     *
     * @param string $path
     * @param ResourceCollection $resources
     * @return Collection
     */
    protected function indexRelationships(string $path, RelationshipCollection $relationships): Collection
    {
        // update IncludesCollection with new child relationships for other queries
        return $relationships->listRelationships()
            ->mapWithKeys(function($relationshipType) use ($path, $relationships){
                $nestedPath = sprintf("%s.%s", $path, $relationshipType);
                return [$nestedPath => $relationships->identifiersFor($relationshipType)]; 
            });
    }

    public function isRoot(string $relationship): bool
    {
        return $relationship === static::ROOT;
    }

    /**
     * Get the parent relationship type
     * 
     * e.g. author.comments => author
     *
     * @param string $relationship
     * @return string
     */
    public static function parent(string $relationship): string 
    {
        $arr = explode('.', $relationship);

        if(count($arr) == 1){
            return static::ROOT;
        }

        unset($arr[count($arr) - 1]);

        return implode('.', $arr);
    }
}