mimmi20/mezzio-navigation-laminasviewrenderer

View on GitHub
src/View/Helper/Navigation/HelperTrait.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\Navigation;

use Laminas\I18n\Exception\RuntimeException;
use Laminas\ServiceManager\ServiceLocatorInterface;
use Laminas\Stdlib\Exception\DomainException;
use Laminas\Stdlib\Exception\InvalidArgumentException;
use Laminas\View\Exception;
use Mimmi20\Mezzio\GenericAuthorization\AuthorizationInterface;
use Mimmi20\Mezzio\Navigation;
use Mimmi20\Mezzio\Navigation\Page\PageInterface;
use Mimmi20\NavigationHelper\Accept\AcceptHelperInterface;
use Mimmi20\NavigationHelper\ContainerParser\ContainerParserInterface;
use Mimmi20\NavigationHelper\FindActive\FindActiveInterface;
use Mimmi20\NavigationHelper\Htmlify\HtmlifyInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;

use function assert;
use function get_debug_type;
use function is_int;
use function sprintf;
use function str_repeat;

/**
 * Base class for navigational helpers.
 *
 * Duck-types against Laminas\I18n\Translator\TranslatorAwareInterface.
 */
trait HelperTrait
{
    /**
     * ContainerInterface to operate on by default
     *
     * @var Navigation\ContainerInterface<PageInterface>|null
     */
    private Navigation\ContainerInterface | null $container = null;
    private string | null $navigation                       = null;
    private LoggerInterface $logger;
    private HtmlifyInterface $htmlify;
    private ContainerParserInterface $containerParser;

    /**
     * The minimum depth a page must have to be included when rendering
     */
    private int | null $minDepth = null;

    /**
     * The maximum depth a page can have to be included when rendering
     */
    private int | null $maxDepth = null;

    /**
     * Indentation string
     */
    private string $indent = '';

    /**
     * Authorization to use when iterating pages
     */
    private AuthorizationInterface | null $authorization = null;

    /**
     * Whether invisible items should be rendered by this helper
     */
    private bool $renderInvisible = false;

    /**
     * Authorization role to use when iterating pages
     */
    private string | null $role = null;
    private ServiceLocatorInterface $serviceLocator;

    /**
     * Whether container should be injected when proxying
     */
    private bool $injectContainer = true;

    /**
     * Whether Authorization should be used for filtering out pages
     */
    private bool $useAuthorization = true;

    /**
     * Default Authorization role to use when iterating pages if not explicitly set in the
     * instance by calling {@link setRole()}
     */
    private static string | null $defaultRole                          = null;
    private static AuthorizationInterface | null $defaultAuthorization = null;

    /**
     * Helper entry point
     *
     * @param Navigation\ContainerInterface<PageInterface>|string|null $container container to operate on
     *
     * @throws InvalidArgumentException
     */
    public function __invoke(Navigation\ContainerInterface | string | null $container = null): self
    {
        if ($container !== null) {
            $this->setContainer($container);
        }

        return $this;
    }

    /**
     * Magic overload: Proxy calls to the navigation container
     *
     * @param string       $method    method name in container
     * @param array<mixed> $arguments rguments to pass
     *
     * @throws void
     */
    public function __call(string $method, array $arguments = []): mixed
    {
        return $this->getContainer()->{$method}(...$arguments);
    }

    /**
     * Magic overload: Proxy to {@link render()}.
     *
     * This method will trigger an E_USER_ERROR if rendering the helper causes
     * an exception to be thrown.
     *
     * Implements {@link ViewHelperInterface::__toString()}.
     *
     * @throws void
     */
    public function __toString(): string
    {
        try {
            return $this->render();
        } catch (Exception\ExceptionInterface | InvalidArgumentException | DomainException | RuntimeException $e) {
            $this->logger->error($e);

            return '';
        }
    }

    /**
     * Sets navigation container the helper operates on by default
     *
     * Implements {@link ViewHelperInterface::setContainer()}.
     *
     * @param Navigation\ContainerInterface<PageInterface>|string|null $container default is null, meaning container will be reset
     *
     * @throws InvalidArgumentException
     */
    public function setContainer(Navigation\ContainerInterface | string | null $container = null): self
    {
        $container = $this->containerParser->parseContainer($container);

        if ($container instanceof Navigation\ContainerInterface || $container === null) {
            $this->container = $container;
        }

        return $this;
    }

    /**
     * Returns the navigation container helper operates on by default
     *
     * Implements {@link ViewHelperInterface::getContainer()}.
     *
     * If no container is set, a new container will be instantiated and
     * stored in the helper.
     *
     * @return Navigation\ContainerInterface<PageInterface> navigation container
     *
     * @throws void
     */
    public function getContainer(): Navigation\ContainerInterface
    {
        if ($this->container === null) {
            $this->container = new Navigation\Navigation();
        }

        return $this->container;
    }

    /**
     * Sets whether container should be injected when proxying
     *
     * @throws void
     */
    public function setInjectContainer(bool $injectContainer = true): self
    {
        $this->injectContainer = $injectContainer;

        return $this;
    }

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

    /**
     * Finds the deepest active page in the given container
     *
     * @param Navigation\ContainerInterface<PageInterface>|string|null $container to search
     * @param int|null                                                 $minDepth  [optional] minimum depth
     *                                                                            required for page to be
     *                                                                            valid. Default is to use
     *                                                                            {@link getMinDepth()}. A
     *                                                                            null value means no minimum
     *                                                                            depth required.
     * @param int|null                                                 $maxDepth  [optional] maximum depth
     *                                                                            a page can have to be
     *                                                                            valid. Default is to use
     *                                                                            {@link getMaxDepth()}. A
     *                                                                            null value means no maximum
     *                                                                            depth required.
     *
     * @return array<string, int|PageInterface|null> an associative array with the values 'depth' and 'page', or an empty array if not found
     * @phpstan-return array{page?: PageInterface|null, depth?: int|null}
     *
     * @throws InvalidArgumentException
     */
    public function findActive(
        Navigation\ContainerInterface | string | null $container,
        int | null $minDepth = null,
        int | null $maxDepth = -1,
    ): array {
        $container = $this->containerParser->parseContainer($container);

        if ($container === null) {
            $container = $this->getContainer();
        }

        if ($minDepth === null) {
            $minDepth = $this->getMinDepth();
        }

        if ((!is_int($maxDepth) || 0 > $maxDepth) && $maxDepth !== null) {
            $maxDepth = $this->getMaxDepth();
        }

        try {
            $findActiveHelper = $this->serviceLocator->build(
                FindActiveInterface::class,
                [
                    'authorization' => $this->getUseAuthorization() ? $this->getAuthorization() : null,
                    'renderInvisible' => $this->getRenderInvisible(),
                    'role' => $this->getRole(),
                ],
            );
        } catch (ContainerExceptionInterface $e) {
            $this->logger->error($e);

            return [];
        }

        assert($findActiveHelper instanceof FindActiveInterface);

        return $findActiveHelper->find($container, $minDepth, $maxDepth);
    }

    // Iterator filter methods:

    /**
     * Determines whether a page should be accepted when iterating
     *
     * Rules:
     * - If a page is not visible it is not accepted, unless RenderInvisible has
     *   been set to true
     * - If $useAuthorization is true (default is true):
     *      - Page is accepted if Authorization returns true, otherwise false
     * - If page is accepted and $recursive is true, the page
     *   will not be accepted if it is the descendant of a non-accepted page
     *
     * @param PageInterface $page      page to check
     * @param bool          $recursive [optional] if true, page will not be
     *                                 accepted if it is the descendant of
     *                                 a page that is not accepted. Default
     *                                 is true
     *
     * @return bool Whether page should be accepted
     *
     * @throws void
     */
    public function accept(PageInterface $page, bool $recursive = true): bool
    {
        try {
            $acceptHelper = $this->serviceLocator->build(
                AcceptHelperInterface::class,
                [
                    'authorization' => $this->getUseAuthorization() ? $this->getAuthorization() : null,
                    'renderInvisible' => $this->getRenderInvisible(),
                    'role' => $this->getRole(),
                ],
            );
        } catch (ContainerExceptionInterface $e) {
            $this->logger->error($e);

            return false;
        }

        assert(
            $acceptHelper instanceof AcceptHelperInterface,
            sprintf(
                '$acceptHelper should be an Instance of %s, but was %s',
                AcceptHelperInterface::class,
                get_debug_type($acceptHelper),
            ),
        );

        return $acceptHelper->accept($page, $recursive);
    }

    /**
     * Returns an HTML string containing an 'a' element for the given page
     *
     * @param PageInterface $page page to generate HTML for
     *
     * @return string HTML string (<a href="…">Label</a>)
     *
     * @throws Exception\InvalidArgumentException
     * @throws RuntimeException
     */
    public function htmlify(PageInterface $page): string
    {
        return $this->htmlify->toHtml(static::class, $page);
    }

    /**
     * Sets AuthorizationInterface to use when iterating pages
     * Implements {@link ViewHelperInterface::setAuthorization()}.
     *
     * @param AuthorizationInterface|null $authorization AuthorizationInterface object
     *
     * @throws void
     */
    public function setAuthorization(AuthorizationInterface | null $authorization = null): self
    {
        $this->authorization = $authorization;

        return $this;
    }

    /**
     * Returns AuthorizationInterface or null if it isn't set using {@link setAuthorization()} or
     * {@link setDefaultAuthorization()}
     *
     * Implements {@link ViewHelperInterface::getAuthorization()}.
     *
     * @return AuthorizationInterface|null AuthorizationInterface object or null
     *
     * @throws void
     */
    public function getAuthorization(): AuthorizationInterface | null
    {
        if (
            !$this->authorization instanceof AuthorizationInterface
            && static::$defaultAuthorization !== null
        ) {
            return static::$defaultAuthorization;
        }

        return $this->authorization;
    }

    /**
     * Checks if the helper has an Authorization instance
     *
     * Implements {@link ViewHelperInterface::hasAuthorization()}.
     *
     * @throws void
     */
    public function hasAuthorization(): bool
    {
        return $this->authorization instanceof AuthorizationInterface
            || static::$defaultAuthorization instanceof AuthorizationInterface;
    }

    /**
     * Checks if the helper has a container
     *
     * Implements {@link ViewHelperInterface::hasContainer()}.
     *
     * @throws void
     */
    public function hasContainer(): bool
    {
        return $this->container !== null;
    }

    /**
     * Set the indentation string for using in {@link render()}, optionally a
     * number of spaces to indent with
     *
     * @throws void
     */
    public function setIndent(int | string $indent): self
    {
        $this->indent = $this->getWhitespace($indent);

        return $this;
    }

    /**
     * Returns indentation
     *
     * @throws void
     */
    public function getIndent(): string
    {
        return $this->indent;
    }

    /**
     * Sets the maximum depth a page can have to be included when rendering
     *
     * @param int|null $maxDepth default is null, which sets no maximum depth
     *
     * @throws void
     */
    public function setMaxDepth(int | null $maxDepth): self
    {
        $this->maxDepth = $maxDepth;

        return $this;
    }

    /**
     * Returns maximum depth a page can have to be included when rendering
     *
     * @throws void
     */
    public function getMaxDepth(): int | null
    {
        return $this->maxDepth;
    }

    /**
     * Sets the minimum depth a page must have to be included when rendering
     *
     * @param int|null $minDepth default is null, which sets no minimum depth
     *
     * @throws void
     */
    public function setMinDepth(int | null $minDepth): self
    {
        $this->minDepth = $minDepth;

        return $this;
    }

    /**
     * Returns minimum depth a page must have to be included when rendering
     *
     * @throws void
     */
    public function getMinDepth(): int | null
    {
        if (!is_int($this->minDepth) || 0 > $this->minDepth) {
            return 0;
        }

        return $this->minDepth;
    }

    /**
     * Render invisible items?
     *
     * @throws void
     */
    public function setRenderInvisible(bool $renderInvisible = true): self
    {
        $this->renderInvisible = $renderInvisible;

        return $this;
    }

    /**
     * Return renderInvisible flag
     *
     * @throws void
     */
    public function getRenderInvisible(): bool
    {
        return $this->renderInvisible;
    }

    /**
     * Sets Authorization role(s) to use when iterating pages
     *
     * Implements {@link ViewHelperInterface::setRole()}.
     *
     * @param string $role [optional] role to set. Expects a string or null. Default is null, which will set no role.
     *
     * @throws void
     */
    public function setRole(string $role): self
    {
        $this->role = $role;

        return $this;
    }

    /**
     * Returns Authorization role to use when iterating pages, or null if it isn't set
     * using {@link setRole()} or {@link setDefaultRole()}
     *
     * Implements {@link ViewHelperInterface::getRole()}.
     *
     * @throws void
     */
    public function getRole(): string | null
    {
        if ($this->role === null && static::$defaultRole !== null) {
            return static::$defaultRole;
        }

        return $this->role;
    }

    /**
     * Checks if the helper has an Authorization role
     *
     * Implements {@link ViewHelperInterface::hasRole()}.
     *
     * @throws void
     */
    public function hasRole(): bool
    {
        return $this->role !== null
            || static::$defaultRole !== null;
    }

    /**
     * Sets whether Authorization should be used
     * Implements {@link ViewHelperInterface::setUseAuthorization()}.
     *
     * @throws void
     */
    public function setUseAuthorization(bool $useAuthorization = true): self
    {
        $this->useAuthorization = $useAuthorization;

        return $this;
    }

    /**
     * Returns whether Authorization should be used
     * Implements {@link ViewHelperInterface::getUseAuthorization()}.
     *
     * @throws void
     */
    public function getUseAuthorization(): bool
    {
        return $this->useAuthorization;
    }

    /** @throws void */
    public function getServiceLocator(): ServiceLocatorInterface
    {
        return $this->serviceLocator;
    }

    // Static methods:

    /**
     * Sets default Authorization to use if another Authorization is not explicitly set
     *
     * @param AuthorizationInterface|null $authorization [optional] Authorization object. Default is null, which
     *                                                   sets no Authorization object.
     *
     * @throws void
     */
    public static function setDefaultAuthorization(AuthorizationInterface | null $authorization = null): void
    {
        static::$defaultAuthorization = $authorization;
    }

    /**
     * Sets default Authorization role(s) to use when iterating pages if not explicitly
     * set later with {@link setRole()}
     *
     * @param string|null $role [optional] role to set. Expects null or string. Default is null, which
     *                          sets no default role.
     *
     * @throws void
     */
    public static function setDefaultRole(string | null $role = null): void
    {
        static::$defaultRole = $role;
    }

    // Util methods:

    /**
     * Retrieve whitespace representation of $indent
     *
     * @throws void
     */
    private function getWhitespace(int | string $indent): string
    {
        if (is_int($indent)) {
            $indent = str_repeat(' ', $indent);
        }

        return $indent;
    }
}