mimmi20/mezzio-navigation-laminasviewrenderer

View on GitHub
src/View/Helper/Navigation.php

Summary

Maintainability
A
25 mins
Test Coverage
A
100%
<?php
/**
 * This file is part of the mimmi20/mezzio-navigation-laminasviewrenderer package.
 *
 * Copyright (c) 2020-2024, Thomas Mueller <mimmi20@live.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types = 1);

namespace Mimmi20\Mezzio\Navigation\LaminasView\View\Helper;

use Laminas\ServiceManager\Exception\InvalidServiceException;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Laminas\ServiceManager\ServiceLocatorInterface;
use Laminas\Stdlib\Exception\DomainException;
use Laminas\View\Exception\InvalidArgumentException;
use Laminas\View\Exception\RuntimeException;
use Laminas\View\Helper\AbstractHtmlElement;
use Laminas\View\Helper\HelperInterface;
use Laminas\View\HelperPluginManager as ViewHelperPluginManager;
use Laminas\View\Renderer\RendererInterface as Renderer;
use Mimmi20\Mezzio\Navigation\ContainerInterface;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\Breadcrumbs;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\HelperTrait;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\Links;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\Menu;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\Sitemap;
use Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\ViewHelperInterface;
use Mimmi20\Mezzio\Navigation\Page\PageInterface;
use Mimmi20\NavigationHelper\ContainerParser\ContainerParserInterface;
use Mimmi20\NavigationHelper\Htmlify\HtmlifyInterface;
use Psr\Log\LoggerInterface;

use function assert;
use function spl_object_hash;
use function sprintf;

/**
 * Proxy helper for retrieving navigational helpers and forwarding calls
 *
 * @method Breadcrumbs breadcrumbs(ContainerInterface|string|null $container = null)
 * @method Links       links(ContainerInterface|string|null $container = null)
 * @method Menu        menu(ContainerInterface|string|null $container = null)
 * @method Sitemap     sitemap(ContainerInterface|string|null $container = null)
 */
final class Navigation extends AbstractHtmlElement implements ViewHelperInterface
{
    use HelperTrait {
        __call as parentCall;
    }

    /**
     * Default proxy to use in {@link render()}
     */
    private string $defaultProxy = 'menu';

    /**
     * Indicates whether a given helper has been injected
     *
     * @var array<string, bool>
     */
    private array $injected = [];

    /**
     * Whether ACL should be injected when proxying
     */
    private bool $injectAuthorization = true;

    /** @var ViewHelperPluginManager<HelperInterface>|null */
    private ViewHelperPluginManager | null $pluginManager = null;

    /** @throws void */
    public function __construct(
        ServiceLocatorInterface $serviceLocator,
        LoggerInterface $logger,
        HtmlifyInterface $htmlify,
        ContainerParserInterface $containerParser,
    ) {
        $this->serviceLocator  = $serviceLocator;
        $this->logger          = $logger;
        $this->htmlify         = $htmlify;
        $this->containerParser = $containerParser;
    }

    /**
     * Magic overload: Proxy to other navigation helpers or the container
     *
     * Examples of usage from a view script or layout:
     * <code>
     * // proxy to Menu helper and render container:
     * echo $this->navigation()->menu();
     *
     * // proxy to Breadcrumbs helper and set indentation:
     * $this->navigation()->breadcrumbs()->setIndent(8);
     *
     * // proxy to container and find all pages with 'blog' route:
     * $blogPages = $this->navigation()->findAllByRoute('blog');
     * </code>
     *
     * @param string                                               $method    helper name or method name in container
     * @param array<ContainerInterface<PageInterface>|string|null> $arguments [optional] arguments to pass
     *
     * @return mixed returns what the proxied call returns
     *
     * @throws InvalidArgumentException
     */
    public function __call(string $method, array $arguments = []): mixed
    {
        // check if call should proxy to another helper
        try {
            $helper = $this->findHelperStrict($method);
        } catch (RuntimeException $e) {
            $this->logger->error($e);

            // default behaviour: proxy call to container
            return $this->parentCall($method, $arguments);
        }

        return $helper(...$arguments);
    }

    /**
     * Renders helper
     *
     * @param ContainerInterface<PageInterface>|string|null $container
     *
     * @throws InvalidArgumentException
     * @throws RuntimeException
     * @throws DomainException
     * @throws \Laminas\Stdlib\Exception\InvalidArgumentException
     */
    public function render(ContainerInterface | string | null $container = null): string
    {
        try {
            $helper = $this->findHelperStrict($this->getDefaultProxy());
        } catch (RuntimeException $e) {
            $this->logger->error($e);

            return '';
        }

        return $helper->render($container);
    }

    /**
     * Returns the helper matching $proxy
     *
     * The helper must implement the interface
     * {@link \Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation}.
     *
     * @param string $proxy  helper name
     * @param bool   $strict [optional] whether exceptions should be
     *                       thrown if something goes
     *                       wrong. Default is true.
     *
     * @return ViewHelperInterface|null helper instance
     *
     * @throws RuntimeException if $strict is true and helper cannot be found
     * @throws InvalidArgumentException
     *
     * @api
     */
    public function findHelper(string $proxy, bool $strict = true): ViewHelperInterface | null
    {
        if ($strict) {
            return $this->findHelperStrict($proxy);
        }

        return $this->findHelperNonStrict($proxy);
    }

    /**
     * Sets the default proxy to use in {@link render()}
     *
     * @param string $proxy default proxy
     *
     * @throws void
     *
     * @api
     */
    public function setDefaultProxy(string $proxy): void
    {
        $this->defaultProxy = $proxy;
    }

    /**
     * Returns the default proxy to use in {@link render()}
     *
     * @throws void
     *
     * @api
     */
    public function getDefaultProxy(): string
    {
        return $this->defaultProxy;
    }

    /**
     * Sets whether Authorization should be injected when proxying
     *
     * @throws void
     *
     * @api
     */
    public function setInjectAuthorization(bool $injectAuthorization = true): void
    {
        $this->injectAuthorization = $injectAuthorization;
    }

    /**
     * Returns whether Authorization should be injected when proxying
     *
     * @throws void
     */
    public function getInjectAuthorization(): bool
    {
        return $this->injectAuthorization;
    }

    /**
     * Set manager for retrieving navigation helpers
     *
     * @param ViewHelperPluginManager<HelperInterface> $pluginManager
     *
     * @throws void
     */
    public function setPluginManager(ViewHelperPluginManager $pluginManager): void
    {
        $renderer = $this->getView();

        if ($renderer) {
            $pluginManager->setRenderer($renderer);
        }

        $this->pluginManager = $pluginManager;
    }

    /**
     * @return ViewHelperPluginManager<HelperInterface>|null
     *
     * @throws void
     *
     * @api
     */
    public function getPluginManager(): ViewHelperPluginManager | null
    {
        return $this->pluginManager;
    }

    /**
     * Set the View object
     *
     * @throws void
     */
    public function setView(Renderer $view): self
    {
        parent::setView($view);

        if ($this->pluginManager) {
            $this->pluginManager->setRenderer($view);
        }

        return $this;
    }

    /**
     * Returns the helper matching $proxy
     *
     * The helper must implement the interface
     * {@link \Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\ViewHelperInterface}.
     *
     * @param string $proxy helper name
     *
     * @return ViewHelperInterface|null helper instance
     *
     * @throws InvalidArgumentException
     */
    private function findHelperNonStrict(string $proxy): ViewHelperInterface | null
    {
        if ($this->pluginManager === null) {
            return null;
        }

        if (!$this->pluginManager->has($proxy)) {
            return null;
        }

        try {
            $helper = $this->pluginManager->get($proxy);
        } catch (ServiceNotFoundException | InvalidServiceException $e) {
            $this->logger->debug($e);

            return null;
        }

        assert($helper instanceof ViewHelperInterface);

        $this->prepareHelper($helper);

        return $helper;
    }

    /**
     * Returns the helper matching $proxy
     *
     * The helper must implement the interface
     * {@link \Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation\ViewHelperInterface}.
     *
     * @param string $proxy helper name
     *
     * @return ViewHelperInterface helper instance
     *
     * @throws RuntimeException if helper cannot be found
     * @throws InvalidArgumentException
     */
    private function findHelperStrict(string $proxy): ViewHelperInterface
    {
        if ($this->pluginManager === null) {
            throw new RuntimeException(
                sprintf('Failed to find plugin for %s, no PluginManager set', $proxy),
            );
        }

        if (!$this->pluginManager->has($proxy)) {
            throw new RuntimeException(
                sprintf('Failed to find plugin for %s', $proxy),
            );
        }

        try {
            $helper = $this->pluginManager->get($proxy);
        } catch (ServiceNotFoundException | InvalidServiceException $e) {
            throw new RuntimeException(
                sprintf('Failed to load plugin for %s', $proxy),
                0,
                $e,
            );
        }

        assert($helper instanceof ViewHelperInterface);

        $this->prepareHelper($helper);

        return $helper;
    }

    /** @throws InvalidArgumentException */
    private function prepareHelper(ViewHelperInterface $helper): void
    {
        $container = $this->getContainer();
        $hash      = spl_object_hash($container) . spl_object_hash($helper);

        if (!isset($this->injected[$hash])) {
            $helper->setContainer();

            $this->inject($helper);

            $this->injected[$hash] = true;
        } elseif ($this->getInjectContainer()) {
            $helper->setContainer($container);
        }
    }

    /**
     * Injects container, ACL, and translator to the given $helper if this
     * helper is configured to do so
     *
     * @param ViewHelperInterface $helper helper instance
     *
     * @throws void
     */
    private function inject(ViewHelperInterface $helper): void
    {
        if ($this->getInjectContainer() && !$helper->hasContainer()) {
            $helper->setContainer($this->getContainer());
        }

        if ($this->getInjectAuthorization() && !$helper->hasAuthorization()) {
            $helper->setAuthorization($this->getAuthorization());
        }

        $role = $this->getRole();

        if ($helper->hasRole() || $role === null) {
            return;
        }

        $helper->setRole($role);
    }
}