netglue/ZF2-Prismic-Module

View on GitHub
src/NetgluePrismic/Mvc/LinkResolver.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

namespace NetgluePrismic\Mvc;

use Prismic\LinkResolver as PrismicResolver;
use Zend\Mvc\Router\RouteStackInterface;
use NetgluePrismic\Mvc\Router\RouterOptions;
use NetgluePrismic\ContextAwareTrait;
use NetgluePrismic\ContextAwareInterface;
use Prismic\Fragment\Link\LinkInterface;
use Prismic\Fragment\Link\DocumentLink;
use NetgluePrismic\Exception;

class LinkResolver extends PrismicResolver implements ContextAwareInterface
{

    use ContextAwareTrait;

    /**
     * RouteStackInterface instance.
     *
     * @var RouteStackInterface
     */
    protected $router;

    /**
     * The array of configured http routes
     * @var array
     */
    protected $routes;

    /**
     * Router Options
     * @var RouterOptions
     */
    protected $routerOptions;

    /**
     * Cached bookmark routes
     * @var array
     */
    protected $bookmarkRouteNames;

    /**
     * Cached mask based routes
     * @var array
     */
    protected $maskRouteNames;

    /**
     * Resolve the given link
     * @param  LinkInterface $link
     * @return string|NULL
     */
    public function resolve($link)
    {
        if (!$link instanceof LinkInterface) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Expected an instance of LinkInterface recieved %s %s',
                gettype($link),
                (is_scalar($link) ? $link : '')
            ));
        }

        if ($link instanceof DocumentLink) {

            // Is the link broken?
            if ($link->isBroken()) {
                return NULL;
            }

            // Is the document a bookmark?
            if ($this->getContext()->isBookmarked($link->getId())) {
                $bookmark = $this->getContext()->findBookmarkByDocument($link->getId());

                try {
                    $routeName = $this->getRouteNameFromBookmark($bookmark);

                    return $this->getRouter()->assemble($this->getRouteParams($link), array(
                        'name' => $routeName,
                    ));
                } catch (Exception\ExceptionInterface $ex) {
                    // It's perfectly acceptable that there might not be a route setup
                    // for the found bookmark.
                }
            }

            // Can we route based on the mask type
            if ($this->hasRouteForMask($link->getType())) {
                $routeName = $this->getRouteNameFromMask($link->getType());
                return $this->getRouter()->assemble($this->getRouteParams($link), array(
                    'name' => $routeName,
                ));
            }

            /**
             * Other possible ways to route... ?
             *
             * It's not sensible to route by matching slugs as we have absolutely no
             * way of making any slug unique or ensuring that it exists in the first place.
             *
             * Slugs are for presenting pretty urls so providing them when building the url
             * is the only real purpose for them.
             *
             * Routing based on collection is feasible and relatively easy to implement.
             * Much the same as masks, but, any given document could be in multiple collections which makes it
             * difficult to discover the route when given a single document.
             *
             * Routing based on fragment text value has potential??
             */

            // Cannot find a specific or generic route for the document
            return NULL;
        }

        return $link->getUrl($this);
    }

    /**
     * Create Prismic specific Route Paramters from the given Document Link according to routing options
     * @param  DocumentLink $link
     * @return array
     */
    public function getRouteParams(DocumentLink $link)
    {
        $document = $this->getContext()->getDocumentById($link->getId());
        $bookmark = $this->getContext()->findBookmarkByDocument($document);

        return array(
            $this->routerOptions->getIdParam() => $link->getId(),
            $this->routerOptions->getUidParam() => $link->getUid(),
            $this->routerOptions->getMaskParam() => $document->getType(),
            $this->routerOptions->getBookmarkParam() => $bookmark,
            $this->routerOptions->getRefParam() => $this->getContext()->getRefAsString(),
            $this->routerOptions->getSlugParam() => $document->getSlug(),
        );
    }

    /**
     * Return the fully qualified route name from a bookmark name
     *
     * @param  string                     $bookmark
     * @return string
     * @throws Exception\RuntimeException if no matching route can be found
     */
    public function getRouteNameFromBookmark($bookmark)
    {
        $bookmarks = $this->getBookmarkedRoutes($this->getRoutes());
        if (!isset($bookmarks[$bookmark])) {
            throw new Exception\RuntimeException(sprintf(
                'No route could be found for the bookmark "%s"',
                $bookmark
            ));
        }

        return $bookmarks[$bookmark];
    }

    /**
     * Return an associative array of bookmark names to route names
     * @return array
     */
    public function getBookmarkedRoutes()
    {
        if (!$this->bookmarkRouteNames) {
            $this->bookmarkRouteNames = $this->findBookmarkedRoutes($this->getRoutes());
        }

        return $this->bookmarkRouteNames;
    }

    /**
     * Search routes for those containing the prismic bookmark parameter in defaults
     *
     * @param  array  $routes Router config array
     * @param  string $parent to help work out the fully qualified route name when called recursively
     * @return array
     */
    protected function findBookmarkedRoutes(array $routes, $parent = '')
    {
        $searchParam = $this->routerOptions->getBookmarkParam();
        $out = array();
        foreach ($routes as $name => $route) {
            $fqrn = trim($parent . '/' . $name, '/');
            $bookmark = isset($route['options']['defaults'][$searchParam]) ? $route['options']['defaults'][$searchParam] : NULL;
            if (!empty($bookmark)) {
                $out[$bookmark] = $fqrn;
            }
            if (isset($route['child_routes']) && count($route['child_routes'])) {
                $out = array_merge($out, $this->findBookmarkedRoutes($route['child_routes'], $name));
            }
        }

        return $out;
    }

    /**
     * Return the last route name that matches the given type of document
     * @param  string      $mask
     * @return string|NULL
     */
    public function getRouteNameFromMask($mask)
    {
        $routes = $this->getMaskRoutes($this->getRoutes());
        if (!isset($routes[$mask])) {
            throw new Exception\RuntimeException('No route could be found for the specified document mask');
        }

        return $routes[$mask];
    }

    /**
     * Whether there is a route for the given mask/type
     * @param  string $mask
     * @return bool
     */
    public function hasRouteForMask($mask)
    {
        $routes = $this->getMaskRoutes($this->getRoutes());

        return isset($routes[$mask]);
    }

    /**
     * Return an associative array of mask names to route names
     * @return array
     */
    public function getMaskRoutes()
    {
        if (!$this->maskRouteNames) {
            $this->maskRouteNames = $this->findRoutesReferencedToMasks($this->getRoutes());
        }

        return $this->maskRouteNames;
    }

    /**
     * Recursively search route config to find those route names that reference a prismic mask/type
     * @param  array  $routes
     * @param  string $parent
     * @return array
     */
    protected function findRoutesReferencedToMasks(array $routes, $parent = '')
    {
        $searchParam = $this->routerOptions->getMaskParam();
        $out = array();
        foreach ($routes as $name => $route) {
            $fqrn = trim($parent . '/' . $name, '/');
            $maskName = isset($route['options']['defaults'][$searchParam]) ? $route['options']['defaults'][$searchParam] : NULL;
            if (!empty($maskName)) {
                $out[$maskName] = $fqrn;
            }
            if (isset($route['child_routes']) && count($route['child_routes'])) {
                $out = array_merge($out, $this->findRoutesReferencedToMasks($route['child_routes'], $name));
            }
        }

        return $out;
    }

    /**
     * Set the router to use for assembling.
     *
     * @param  RouteStackInterface $router
     * @return void
     */
    public function setRouter(RouteStackInterface $router)
    {
        $this->router = $router;
    }

    /**
     * Return the application's router
     * @return RouteStackInterface
     */
    public function getRouter()
    {
        return $this->router;
    }

    /**
     * Set Router Options so we can figure out parameter names that correspond to the request/route
     * @param RouterOptions $options
     */
    public function setRouterOptions(RouterOptions $options)
    {
        $this->bookmarkRouteNames = $this->maskRouteNames = NULL;
        $this->routerOptions = $options;
    }

    /**
     * Provide the configured routes.
     *
     * It's nigh on impossible to interrogate routes from the router object
     * because there are no interface methods to retrieve child routes, or get route default params etc
     * @param  array $routes
     * @return void
     */
    public function setRoutes(array $routes)
    {
        $this->bookmarkRouteNames = $this->maskRouteNames = NULL;
        $this->routes = $routes;
    }

    /**
     * Return the injected router configuration array
     * @return array
     */
    public function getRoutes()
    {
        return $this->routes;
    }
}