diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml new file mode 100644 index 0000000..3da9965 --- /dev/null +++ b/.github/workflows/cs-tests.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run phpcs checks + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpcs checks + run: vendor/bin/phpcs diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..74550fc --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run static analysis + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run static analysis + run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..d2ab8e7 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,47 @@ +on: + - push + +name: Run PHPUnit tests + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always diff --git a/.gitignore b/.gitignore index c4a2a74..4d3fdcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ clover.xml coveralls-upload.json -phpunit.xml +.phpcs-cache +.phpunit.result.cache # Created by .ignore support plugin (hsz.mobi) ### JetBrains template diff --git a/README.md b/README.md index 58373b6..8e45b4a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ # dot-navigation  - + [](https://github.com/dotkernel/dot-navigation/issues) [](https://github.com/dotkernel/dot-navigation/network) [](https://github.com/dotkernel/dot-navigation/stargazers) -[](https://github.com/dotkernel/dot-navigation/blob/3.2.0/LICENSE.md) +[](https://github.com/dotkernel/dot-navigation/blob/3.0/LICENSE.md) + +[](https://insight.symfony.com/projects/68b7c728-4cc9-40ac-a3be-cf17f9b2eaf1) + Allows you to easily define and parse menus inside templates, configuration based approach. ## Installation Run -```bash -$ composer require dotkernel/dot-navigation -``` + + composer require dotkernel/dot-navigation Merge `ConfigProvider` to your application's configuration. @@ -26,59 +28,8 @@ Register `NavigationMiddleware` in your middleware pipe between the routing and ## Configuration -In your `config/autoload` directory, create a config file - -##### navigation.global.php -```php -return [ - 'dot_navigation' => [ - //enable menu item active if any child is active - 'active_recursion' => true, - - //map a provider name to its config - 'containers' => [ - 'default' => [ - 'type' => 'ArrayProvider', - 'options' => [ - 'items' => [ - [ - 'options' => [ - 'label' => 'Menu #1', - 'route' => [ - 'route_name' => 'home', - 'route_params' => [], - 'query_params' => [], - 'fragment_id' => null, - 'options' => [], - - //the below parameters are not used in route generation - //they are used in finding if a page is active by omitting some parameters from the check - 'ignore_params' => [] - ], - ], - 'attributes' => [ - 'name' => 'Menu #1', - ] - ], - [ - 'options' => [ - 'label' => 'Menu #2', - 'route' => ['route_name' => 'home'/*,...*/], - ], - 'attributes' => [ - 'name' => 'Menu #1', - ] - ] - ], - ], - ], - ], - - //register custom providers here - 'provider_manager' => [], - ], -]; -``` +Locate dot-navigation's distributable config file `vendor/dotkernel/dot-navigation/config/autoload/navigation.global.php.dist` and duplicate it in your project as `config/autoload/navigation.global.php` + ## Components diff --git a/composer.json b/composer.json index 3c2a842..ad70a35 100644 --- a/composer.json +++ b/composer.json @@ -17,20 +17,26 @@ "email": "team@dotkernel.com" } ], + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, "require": { - "php": "^7.4 || ~8.0.0 || ~8.1.0", - "psr/http-message": "^1.0.1", - "psr/http-server-middleware": "^1.0.1", - "laminas/laminas-servicemanager": "^3.11.2", + "php": "~8.1.0 || ~8.2.0", + "dotkernel/dot-authorization": "^3.1.0", + "dotkernel/dot-helpers": "^3.2.0", "laminas/laminas-escaper": "^2.10.0", + "laminas/laminas-servicemanager": "^3.11.2", "mezzio/mezzio-template": "^2.4.0", - "dotkernel/dot-helpers": "^3.2.0" + "psr/http-message": "^1.0.1", + "psr/http-server-middleware": "^1.0.1" }, "require-dev": { - "phpunit/phpunit": "^9.5.20", - "squizlabs/php_codesniffer": "^3.6.2", - "laminas/laminas-stdlib": "^3.7.1", - "dotkernel/dot-authorization": "^3.1.0" + "laminas/laminas-coding-standard": "^2.5", + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { @@ -41,5 +47,16 @@ "psr-4": { "DotTest\\Navigation\\": "test/" } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", + "static-analysis": "psalm --shepherd --stats" } } diff --git a/navigation.global.php.dist b/config/autoload/navigation.global.php.dist similarity index 100% rename from navigation.global.php.dist rename to config/autoload/navigation.global.php.dist diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1efe663 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> + + <arg name="basepath" value="."/> + <arg name="cache" value=".phpcs-cache"/> + <arg name="colors"/> + <arg name="extensions" value="php"/> + <arg name="parallel" value="80"/> + + <!-- Show progress --> + <arg value="p"/> + + <!-- Paths to check --> + <file>src</file> + <file>test</file> + + <!-- Include all rules from the Laminas Coding Standard --> + <rule ref="LaminasCodingStandard"/> +</ruleset> diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6de330b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" + bootstrap="./vendor/autoload.php" + colors="true"> + <testsuites> + <testsuite name="Dot-Navigation Test Suite"> + <directory>./test</directory> + </testsuite> + </testsuites> + <coverage/> + <source> + <include> + <directory suffix=".php">./src</directory> + </include> + </source> +</phpunit> diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..915d9e0 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<files psalm-version="5.13.1@086b94371304750d1c673315321a55d15fc59015"> + <file src="src/Filter/IsAllowedFilter.php"> + <MissingTemplateParam> + <code>IsAllowedFilter</code> + </MissingTemplateParam> + </file> + <file src="src/NavigationContainer.php"> + <MissingTemplateParam> + <code>RecursiveIterator</code> + </MissingTemplateParam> + </file> + <file src="src/Provider/ProviderPluginManager.php"> + <MissingTemplateParam> + <code>ProviderPluginManager</code> + </MissingTemplateParam> + </file> +</files> diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9dd8f07 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<psalm + errorLevel="4" + resolveFromConfigFile="true" + findUnusedCode="false" + findUnusedBaselineEntry="true" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="https://getpsalm.org/schema/config" + xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + errorBaseline="psalm-baseline.xml" +> + <projectFiles> + <directory name="src" /> + <ignoreFiles> + <directory name="vendor" /> + </ignoreFiles> + </projectFiles> +</psalm> diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 698f6fc..185e1e4 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,11 +1,6 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation; @@ -15,54 +10,43 @@ use Dot\Navigation\Factory\NavigationServiceFactory; use Dot\Navigation\Factory\ProviderPluginManagerFactory; use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Provider\Factory; +use Dot\Navigation\Provider\FactoryInterface; use Dot\Navigation\Provider\ProviderPluginManager; use Dot\Navigation\Service\Navigation; use Dot\Navigation\Service\NavigationInterface; use Dot\Navigation\View\NavigationRenderer; use Dot\Navigation\View\RendererInterface; -/** - * Class ConfigProvider - * @package Dot\Navigation - */ class ConfigProvider { - /** - * @return array - */ public function __invoke(): array { return [ - 'dependencies' => $this->getDependencyConfig(), - + 'dependencies' => $this->getDependencyConfig(), 'dot_navigation' => [ - 'active_recursion' => true, - - 'containers' => [], - - 'provider_manager' => [] + 'containers' => [], + 'provider_manager' => [], ], ]; } - /** - * @return array - */ public function getDependencyConfig(): array { return [ 'factories' => [ - NavigationOptions::class => NavigationOptionsFactory::class, + Navigation::class => NavigationServiceFactory::class, + NavigationMiddleware::class => NavigationMiddlewareFactory::class, + NavigationOptions::class => NavigationOptionsFactory::class, + NavigationRenderer::class => NavigationRendererFactory::class, ProviderPluginManager::class => ProviderPluginManagerFactory::class, - Navigation::class => NavigationServiceFactory::class, - NavigationRenderer::class => NavigationRendererFactory::class, - NavigationMiddleware::class => NavigationMiddlewareFactory::class, ], - 'aliases' => [ + 'aliases' => [ + FactoryInterface::class => Factory::class, NavigationInterface::class => Navigation::class, - RendererInterface::class => NavigationRenderer::class, - ] + RendererInterface::class => NavigationRenderer::class, + ], ]; } } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index d6926ec..8e51657 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -1,19 +1,9 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Exception; -/** - * Interface ExceptionInterface - * @package Dot\Navigation\Exception - */ interface ExceptionInterface { - } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 94cfc34..865c4b4 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -1,19 +1,9 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Exception; -/** - * Class InvalidArgumentException - * @package Dot\Navigation\Exception - */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { - } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 0549de2..ea835b3 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -1,19 +1,9 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Exception; -/** - * Class RuntimeException - * @package Dot\Navigation\Exception - */ class RuntimeException extends \RuntimeException implements ExceptionInterface { - } diff --git a/src/Factory/NavigationMiddlewareFactory.php b/src/Factory/NavigationMiddlewareFactory.php index 0d91d65..1069078 100644 --- a/src/Factory/NavigationMiddlewareFactory.php +++ b/src/Factory/NavigationMiddlewareFactory.php @@ -1,32 +1,33 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Factory; use Dot\Navigation\NavigationMiddleware; use Dot\Navigation\Service\NavigationInterface; +use Exception; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; -/** - * Class NavigationMiddlewareFactory - * @package Dot\Navigation\Factory - */ class NavigationMiddlewareFactory { + public const MESSAGE_MISSING_NAVIGATION = 'Unable to find NavigationInterface in the container'; + /** - * @param ContainerInterface $container - * @param $requestedName - * @return NavigationMiddleware + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception */ - public function __invoke(ContainerInterface $container, $requestedName): NavigationMiddleware + public function __invoke(ContainerInterface $container): NavigationMiddleware { - $navigation = $container->get(NavigationInterface::class); - return new $requestedName($navigation); + if (! $container->has(NavigationInterface::class)) { + throw new Exception(self::MESSAGE_MISSING_NAVIGATION); + } + + return new NavigationMiddleware( + $container->get(NavigationInterface::class) + ); } } diff --git a/src/Factory/NavigationOptionsFactory.php b/src/Factory/NavigationOptionsFactory.php index 1915108..71f7826 100644 --- a/src/Factory/NavigationOptionsFactory.php +++ b/src/Factory/NavigationOptionsFactory.php @@ -1,31 +1,45 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Factory; use Dot\Navigation\Options\NavigationOptions; +use Exception; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +use function array_key_exists; +use function is_array; -/** - * Class NavigationOptionsFactory - * @package Dot\Navigation\Factory - */ class NavigationOptionsFactory { + public const MESSAGE_MISSING_CONFIG = 'Unable to find config in the container'; + public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-navigation config'; + /** - * @param ContainerInterface $container - * @param $requestedName - * @return NavigationOptions + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception */ - public function __invoke(ContainerInterface $container, $requestedName): NavigationOptions + public function __invoke(ContainerInterface $container): NavigationOptions { - $config = $container->get('config')['dot_navigation']; - return new $requestedName($config); + if (! $container->has('config')) { + throw new Exception(self::MESSAGE_MISSING_CONFIG); + } + $config = $container->get('config'); + + if ( + ! array_key_exists('dot_navigation', $config) + || ! is_array($config['dot_navigation']) + || empty($config['dot_navigation']) + ) { + throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG); + } + + return new NavigationOptions( + $config['dot_navigation'] + ); } } diff --git a/src/Factory/NavigationRendererFactory.php b/src/Factory/NavigationRendererFactory.php index 64fbdf1..1180500 100644 --- a/src/Factory/NavigationRendererFactory.php +++ b/src/Factory/NavigationRendererFactory.php @@ -1,37 +1,47 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Factory; use Dot\Navigation\Options\NavigationOptions; use Dot\Navigation\Service\NavigationInterface; use Dot\Navigation\View\NavigationRenderer; -use Psr\Container\ContainerInterface; +use Exception; use Mezzio\Template\TemplateRendererInterface; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; -/** - * Class NavigationRendererFactory - * @package Dot\Navigation\Factory - */ class NavigationRendererFactory { + public const MESSAGE_MISSING_NAVIGATION_INTERFACE = 'Unable to find NavigationInterface in the container'; + public const MESSAGE_MISSING_NAVIGATION_OPTIONS = 'Unable to find NavigationOptions in the container'; + public const MESSAGE_MISSING_TEMPLATE_RENDERER = 'Unable to find TemplateRendererInterface in the container'; + /** - * @param ContainerInterface $container - * @param $requestedName - * @return NavigationRenderer + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + * @throws Exception */ - public function __invoke(ContainerInterface $container, $requestedName): NavigationRenderer + public function __invoke(ContainerInterface $container): NavigationRenderer { - $options = $container->get(NavigationOptions::class); - $navigation = $container->get(NavigationInterface::class); - $template = $container->get(TemplateRendererInterface::class); + if (! $container->has(NavigationInterface::class)) { + throw new Exception(self::MESSAGE_MISSING_NAVIGATION_INTERFACE); + } + + if (! $container->has(TemplateRendererInterface::class)) { + throw new Exception(self::MESSAGE_MISSING_TEMPLATE_RENDERER); + } + + if (! $container->has(NavigationOptions::class)) { + throw new Exception(self::MESSAGE_MISSING_NAVIGATION_OPTIONS); + } - return new $requestedName($navigation, $template, $options); + return new NavigationRenderer( + $container->get(NavigationInterface::class), + $container->get(TemplateRendererInterface::class), + $container->get(NavigationOptions::class) + ); } } diff --git a/src/Factory/NavigationServiceFactory.php b/src/Factory/NavigationServiceFactory.php index a7a0bf2..4019809 100644 --- a/src/Factory/NavigationServiceFactory.php +++ b/src/Factory/NavigationServiceFactory.php @@ -1,11 +1,6 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Factory; @@ -15,33 +10,47 @@ use Dot\Navigation\Provider\Factory; use Dot\Navigation\Provider\ProviderPluginManager; use Dot\Navigation\Service\Navigation; +use Exception; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; -/** - * Class NavigationServiceFactory - * @package Dot\Navigation\Factory - */ class NavigationServiceFactory { + public const MESSAGE_MISSING_PLUGIN_MANAGER = 'Unable to find RouteHelper in the container'; + public const MESSAGE_MISSING_ROUTE_HELPER = 'Unable to find ProviderPluginManager in the container'; + public const MESSAGE_MISSING_NAVIGATION_OPTIONS = 'Unable to find NavigationOptions in the container'; + /** - * @param ContainerInterface $container - * @param $requestedName - * @return Navigation + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + * @throws Exception */ - public function __invoke(ContainerInterface $container, $requestedName): Navigation + public function __invoke(ContainerInterface $container): Navigation { - $routeHelper = $container->get(RouteHelper::class); - $authorization = $container->has(AuthorizationInterface::class) - ? $container->get(AuthorizationInterface::class) - : null; + if (! $container->has(RouteHelper::class)) { + throw new Exception(self::MESSAGE_MISSING_ROUTE_HELPER); + } + + if (! $container->has(ProviderPluginManager::class)) { + throw new Exception(self::MESSAGE_MISSING_PLUGIN_MANAGER); + } - $providerFactory = new Factory($container, $container->get(ProviderPluginManager::class)); + if (! $container->has(NavigationOptions::class)) { + throw new Exception(self::MESSAGE_MISSING_NAVIGATION_OPTIONS); + } /** @var NavigationOptions $options */ $options = $container->get(NavigationOptions::class); - /** @var Navigation $service */ - $service = new $requestedName($providerFactory, $routeHelper, $options, $authorization); + $service = new Navigation( + new Factory($container, $container->get(ProviderPluginManager::class)), + $container->get(RouteHelper::class), + $options, + $container->has(AuthorizationInterface::class) + ? $container->get(AuthorizationInterface::class) + : null + ); $service->setIsActiveRecursion($options->getActiveRecursion()); return $service; diff --git a/src/Factory/ProviderPluginManagerFactory.php b/src/Factory/ProviderPluginManagerFactory.php index 1d337b6..aaa7dc3 100644 --- a/src/Factory/ProviderPluginManagerFactory.php +++ b/src/Factory/ProviderPluginManagerFactory.php @@ -1,30 +1,53 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Factory; use Dot\Navigation\Provider\ProviderPluginManager; +use Exception; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +use function array_key_exists; +use function is_array; -/** - * Class ProviderPluginManagerFactory - * @package Dot\Navigation\Factory - */ class ProviderPluginManagerFactory { + public const MESSAGE_MISSING_CONFIG = 'Unable to find config in the container'; + public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-navigation config'; + public const MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER = 'Missing/invalid dot-navigation config: provider_manager'; + /** - * @param ContainerInterface $container - * @return ProviderPluginManager + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception */ - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container): ProviderPluginManager { - $config = $container->get('config')['dot_navigation']['provider_manager']; + if (! $container->has('config')) { + throw new Exception(self::MESSAGE_MISSING_CONFIG); + } + $config = $container->get('config'); + + if ( + ! array_key_exists('dot_navigation', $config) + || ! is_array($config['dot_navigation']) + || empty($config['dot_navigation']) + ) { + throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG); + } + $config = $config['dot_navigation']; + + if ( + ! array_key_exists('provider_manager', $config) + || ! is_array($config['provider_manager']) + ) { + throw new Exception(self::MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER); + } + $config = $config['provider_manager']; + return new ProviderPluginManager($container, $config); } } diff --git a/src/Filter/IsAllowedFilter.php b/src/Filter/IsAllowedFilter.php index 7937ed9..3d548e6 100644 --- a/src/Filter/IsAllowedFilter.php +++ b/src/Filter/IsAllowedFilter.php @@ -1,50 +1,31 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Filter; use Dot\Navigation\Service\NavigationInterface; +use RecursiveFilterIterator; +use RecursiveIterator; -/** - * Class IsAllowedFilter - * @package Dot\Navigation\Filter - */ -class IsAllowedFilter extends \RecursiveFilterIterator +class IsAllowedFilter extends RecursiveFilterIterator { - /** @var NavigationInterface */ - protected $navigation; + protected NavigationInterface $navigation; - /** - * IsAllowedFilter constructor. - * @param \RecursiveIterator $iterator - * @param NavigationInterface $navigation - */ - public function __construct(\RecursiveIterator $iterator, NavigationInterface $navigation) + public function __construct(RecursiveIterator $iterator, NavigationInterface $navigation) { $this->navigation = $navigation; parent::__construct($iterator); } - /** - * @return bool - */ public function accept(): bool { return $this->navigation->isAllowed($this->current()); } - /** - * @return IsAllowedFilter - */ public function getChildren(): IsAllowedFilter { - /** @var \RecursiveIterator $innerIterator */ + /** @var RecursiveIterator $innerIterator */ $innerIterator = $this->getInnerIterator(); return new self($innerIterator->getChildren(), $this->navigation); } diff --git a/src/NavigationContainer.php b/src/NavigationContainer.php index 409c7e1..7b4fec4 100644 --- a/src/NavigationContainer.php +++ b/src/NavigationContainer.php @@ -1,191 +1,129 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation; -/** - * Class Container - * @package Dot\Navigation - */ -class NavigationContainer implements \RecursiveIterator -{ - /** - * Index of current active child - * @var int - */ - protected $index = 0; +use RecursiveIterator; +use RecursiveIteratorIterator; - /** - * Child nodes - * @var array - */ - protected $children = []; +use function count; + +class NavigationContainer implements RecursiveIterator +{ + protected int $index = 0; + protected array $children = []; - /** - * NavigationContainer constructor. - * @param array $pages - */ public function __construct(array $pages = []) { $this->addPages($pages); } /** - * @param array $pages + * @param Page[] $pages */ - public function addPages(array $pages) + public function addPages(array $pages): void { foreach ($pages as $page) { $this->addPage($page); } } - /** - * @param Page $page - */ - public function addPage(Page $page) + public function addPage(Page $page): void { $this->children[] = $page; } - /** - * @return NavigationContainer - */ public function current(): NavigationContainer { return $this->children[$this->index]; } - /** - * Increment current position to the next element - */ - public function next() + public function next(): void { $this->index++; } - /** - * @return int - */ public function key(): int { return $this->index; } - /** - * @return bool - */ public function valid(): bool { return isset($this->children[$this->index]); } - /** - * Reset position to the first element - */ - public function rewind() + public function rewind(): void { $this->index = 0; } - /** - * @return bool - */ public function hasChildren(): bool { return count($this->children) > 0; } - /** - * @return NavigationContainer - */ public function getChildren(): NavigationContainer { return $this->children[$this->index]; } - /** - * Find a single child by attribute - * - * @param string $attribute - * @param mixed $value - * @return Page|null - */ - public function findOneByAttribute(string $attribute, $value): ?Page + public function findOneByAttribute(string $attribute, mixed $value): ?Page { - $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); + /** @var Page $page */ foreach ($iterator as $page) { if ($page->getAttribute($attribute) === $value) { return $page; } } + return null; } - /** - * Find all children by attribute - * - * @param string $attribute - * @param mixed $value - * @return array - */ - public function findByAttribute(string $attribute, $value): array + public function findByAttribute(string $attribute, mixed $value): array { - $result = []; - $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); + $result = []; + $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); /** @var Page $page */ foreach ($iterator as $page) { - if ($page->getAttribute($attribute) == $value) { + if ($page->getAttribute($attribute) === $value) { $result[] = $page; } } + return $result; } - /** - * Finds a single child by option. - * - * @param string $option - * @param mixed $value - * @return Page|null - */ - public function findOneByOption(string $option, $value): ?Page + public function findOneByOption(string $option, mixed $value): ?Page { - $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); + /** @var Page $page */ foreach ($iterator as $page) { - if ($page->getOption($option) == $value) { + if ($page->getOption($option) === $value) { return $page; } } + return null; } - /** - * Finds all children by option. - * - * @param string $option - * @param mixed $value - * @return array - */ - public function findByOption(string $option, $value): array + public function findByOption(string $option, mixed $value): array { - $result = []; - $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); + $result = []; + $iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); + /** @var Page $page */ foreach ($iterator as $page) { - if ($page->getOption($option) == $value) { + if ($page->getOption($option) === $value) { $result[] = $page; } } + return $result; } } diff --git a/src/NavigationMiddleware.php b/src/NavigationMiddleware.php index 9247ac4..dfc2471 100644 --- a/src/NavigationMiddleware.php +++ b/src/NavigationMiddleware.php @@ -1,50 +1,29 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation; use Dot\Navigation\Service\NavigationInterface; +use Mezzio\Router\RouteResult; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Mezzio\Router\RouteResult; -/** - * Class NavigationMiddleware - * @package Dot\Navigation - */ class NavigationMiddleware implements MiddlewareInterface { - /** - * @var NavigationInterface - */ - protected $navigation; + protected NavigationInterface $navigation; - /** - * NavigationMiddleware constructor. - * @param NavigationInterface $navigation - */ public function __construct(NavigationInterface $navigation) { $this->navigation = $navigation; } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface - */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $routeResult = $request->getAttribute(RouteResult::class, null); - if ($routeResult) { + $routeResult = $request->getAttribute(RouteResult::class); + if ($routeResult instanceof RouteResult) { $this->navigation->setRouteResult($routeResult); } diff --git a/src/Options/NavigationOptions.php b/src/Options/NavigationOptions.php index a8715b9..dae8698 100644 --- a/src/Options/NavigationOptions.php +++ b/src/Options/NavigationOptions.php @@ -1,66 +1,38 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Options; use Laminas\Stdlib\AbstractOptions; -/** - * Class NavigationOptions - * @package Dot\Navigation\Options - */ class NavigationOptions extends AbstractOptions { - /** @var array */ - protected $containers; + protected array $containers = []; + protected bool $activeRecursion = true; - /** @var bool */ - protected $activeRecursion = true; - - /** - * NavigationOptions constructor. - * @param array $options - */ - public function __construct($options = null) + public function __construct(?array $options = null) { $this->__strictMode__ = false; parent::__construct($options); } - /** - * @return mixed - */ - public function getContainers() + public function getContainers(): array { return $this->containers; } - /** - * @param mixed $containers - */ - public function setContainers($containers) + public function setContainers(array $containers): void { $this->containers = $containers; } - /** - * @return boolean - */ - public function getActiveRecursion() + public function getActiveRecursion(): bool { return $this->activeRecursion; } - /** - * @param boolean $activeRecursion - */ - public function setActiveRecursion($activeRecursion) + public function setActiveRecursion(bool $activeRecursion): void { $this->activeRecursion = $activeRecursion; } diff --git a/src/Page.php b/src/Page.php index 61ae17b..aeac98c 100644 --- a/src/Page.php +++ b/src/Page.php @@ -1,153 +1,98 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation; -/** - * Class Page - * @package Dot\Navigation - */ +use function count; +use function is_string; + class Page extends NavigationContainer { - /** - * @var array - */ - protected $attributes = []; - - /** - * @var array - */ - protected $options = []; - - /** - * @var null|Page - */ - protected $parent; - - /** - * @return bool - */ + protected ?Page $parent = null; + protected array $attributes = []; + protected array $options = []; + public function hasParent(): bool { - return null !== $this->parent; + return $this->parent instanceof Page; } - /** - * @return Page|null - */ public function getParent(): ?Page { return $this->parent; } - /** - * @param NavigationContainer $parent - */ - public function setParent(NavigationContainer $parent) + public function setParent(Page $parent): void { $this->parent = $parent; } - /** - * @param Page $page - */ - public function addPage(Page $page) + public function addPage(Page $page): void { $page->setParent($this); parent::addPage($page); } - /** - * @param string $option - * @param mixed $value - */ - public function setOption(string $option, $value) + public function setOption(string $option, mixed $value): void { $this->options[$option] = $value; } - /** - * @return array - */ public function getOptions(): array { return $this->options; } - /** - * @param array $options - */ - public function setOptions(array $options) + public function hasOptions(): bool + { + return count($this->options) > 0; + } + + public function setOptions(array $options): void { $this->options = $options; } - /** - * @param string $attribute - * @param mixed $value - */ - public function setAttribute(string $attribute, $value) + public function setAttribute(string $attribute, mixed $value): void { $this->attributes[$attribute] = $value; } - /** - * @param string $attribute - * @return mixed - */ - public function getAttribute(string $attribute) + public function getAttribute(string $attribute): mixed { return $this->attributes[$attribute] ?? null; } - /** - * @return array - */ public function getAttributes(): array { return $this->attributes; } - /** - * @param array $attributes - */ - public function setAttributes(array $attributes) + public function hasAttributes(): bool + { + return count($this->attributes) > 0; + } + + public function setAttributes(array $attributes): void { $this->attributes = $attributes; } - /** - * @return string - */ public function getName(): ?string { return $this->getOption('name'); } - /** - * @param string $option - * @return mixed - */ - public function getOption(string $option) + public function getOption(string $option): mixed { return $this->options[$option] ?? null; } - /** - * @return string - */ public function getLabel(): string { $label = $this->getOption('label'); - if (!is_string($label) || empty($label)) { - $label = 'Not defined'; - } - return $label; + + return ! is_string($label) || empty($label) ? 'Not defined' : $label; } } diff --git a/src/Provider/ArrayProvider.php b/src/Provider/ArrayProvider.php index fe5fade..22c3b02 100644 --- a/src/Provider/ArrayProvider.php +++ b/src/Provider/ArrayProvider.php @@ -1,48 +1,27 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Provider; use Dot\Navigation\NavigationContainer; use Dot\Navigation\Page; -/** - * Class ArrayProvider - * @package Dot\Navigation\Provider - */ +use function count; +use function is_array; + class ArrayProvider implements ProviderInterface { - /** - * @var NavigationContainer - */ - protected $container; - - /** - * @var array - */ - protected $items = []; - - /** - * ArrayProvider constructor. - * @param array $options - */ - public function __construct(array $options = null) + protected ?NavigationContainer $container = null; + protected array $items = []; + + public function __construct(?array $options = []) { - $options = $options ?? []; if (isset($options['items']) && is_array($options['items'])) { $this->setItems($options['items']); } } - /** - * @return NavigationContainer - */ public function getContainer(): NavigationContainer { if ($this->container instanceof NavigationContainer) { @@ -58,10 +37,6 @@ public function getContainer(): NavigationContainer return $this->container; } - /** - * @param array $spec - * @return Page - */ protected function getPage(array $spec): Page { $page = new Page(); @@ -83,18 +58,17 @@ protected function getPage(array $spec): Page return $page; } - /** - * @return array - */ public function getItems(): array { return $this->items; } - /** - * @param array $items - */ - public function setItems(array $items) + public function hasItems(): bool + { + return count($this->items) > 0; + } + + public function setItems(array $items): void { $this->items = $items; } diff --git a/src/Provider/Factory.php b/src/Provider/Factory.php index 7f4398c..7944eed 100644 --- a/src/Provider/Factory.php +++ b/src/Provider/Factory.php @@ -1,44 +1,23 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Provider; use Dot\Navigation\Exception\RuntimeException; use Psr\Container\ContainerInterface; -/** - * Class Factory - * @package Dot\Navigation\Provider - */ -class Factory +class Factory implements FactoryInterface { - /** @var ContainerInterface */ - protected $container; + protected ContainerInterface $container; + protected ?ProviderPluginManager $providerPluginManager; - /** @var ProviderPluginManager */ - protected $providerPluginManager; - - /** - * Factory constructor. - * @param ContainerInterface $container - * @param ProviderPluginManager|null $providerPluginManager - */ - public function __construct(ContainerInterface $container, ProviderPluginManager $providerPluginManager = null) + public function __construct(ContainerInterface $container, ?ProviderPluginManager $providerPluginManager = null) { - $this->container = $container; + $this->container = $container; $this->providerPluginManager = $providerPluginManager; } - /** - * @param array $specs - * @return ProviderInterface - */ public function create(array $specs): ProviderInterface { $type = $specs['type'] ?? ''; @@ -46,16 +25,12 @@ public function create(array $specs): ProviderInterface throw new RuntimeException('Undefined navigation provider type'); } - $options = $specs['options'] ?? null; - return $this->getProviderPluginManager()->get($type, $options); + return $this->getProviderPluginManager()->get($type, $specs['options'] ?? null); } - /** - * @return ProviderPluginManager - */ public function getProviderPluginManager(): ProviderPluginManager { - if (!$this->providerPluginManager) { + if (! $this->providerPluginManager instanceof ProviderPluginManager) { $this->providerPluginManager = new ProviderPluginManager($this->container, []); } diff --git a/src/Provider/FactoryInterface.php b/src/Provider/FactoryInterface.php new file mode 100644 index 0000000..5968781 --- /dev/null +++ b/src/Provider/FactoryInterface.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Dot\Navigation\Provider; + +interface FactoryInterface +{ + public function create(array $specs): ProviderInterface; + + public function getProviderPluginManager(): ProviderPluginManager; +} diff --git a/src/Provider/ProviderInterface.php b/src/Provider/ProviderInterface.php index 664eb1d..db0689f 100644 --- a/src/Provider/ProviderInterface.php +++ b/src/Provider/ProviderInterface.php @@ -1,24 +1,12 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Provider; use Dot\Navigation\NavigationContainer; -/** - * Interface ProviderInterface - * @package Dot\Navigation\Provider - */ interface ProviderInterface { - /** - * @return NavigationContainer - */ public function getContainer(): NavigationContainer; } diff --git a/src/Provider/ProviderPluginManager.php b/src/Provider/ProviderPluginManager.php index bf61a5d..4ea540c 100644 --- a/src/Provider/ProviderPluginManager.php +++ b/src/Provider/ProviderPluginManager.php @@ -1,24 +1,15 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Provider; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Factory\InvokableFactory; -/** - * Class ProviderPluginManager - * @package Dot\Navigation\Provider - */ class ProviderPluginManager extends AbstractPluginManager { - /** @var string */ + /** @var string $instanceOf */ protected $instanceOf = ProviderInterface::class; /** @var array */ @@ -26,11 +17,12 @@ class ProviderPluginManager extends AbstractPluginManager ArrayProvider::class => InvokableFactory::class, ]; + /** @var string[] $aliases */ protected $aliases = [ 'arrayprovider' => ArrayProvider::class, 'arrayProvider' => ArrayProvider::class, 'ArrayProvider' => ArrayProvider::class, - 'array' => ArrayProvider::class, - 'Array' => ArrayProvider::class, + 'array' => ArrayProvider::class, + 'Array' => ArrayProvider::class, ]; } diff --git a/src/Service/Navigation.php b/src/Service/Navigation.php index 85b5bba..57792b0 100644 --- a/src/Service/Navigation.php +++ b/src/Service/Navigation.php @@ -1,11 +1,6 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Service; @@ -15,117 +10,72 @@ use Dot\Navigation\NavigationContainer; use Dot\Navigation\Options\NavigationOptions; use Dot\Navigation\Page; -use Dot\Navigation\Provider\Factory; -use Dot\Navigation\Provider\ProviderInterface; +use Dot\Navigation\Provider\FactoryInterface; use Mezzio\Router\RouteResult; +use RecursiveIteratorIterator; + +use function array_diff; +use function array_merge; +use function spl_object_hash; +use function sprintf; -/** - * Class Navigation - * @package Dot\Navigation\Service - */ class Navigation implements NavigationInterface { - /** - * @var NavigationContainer[] - */ - protected $containers = []; - - /** - * @var Factory - */ - protected $providerFactory; - - /** @var RouteHelper */ - protected $routeHelper; - - /** - * @var NavigationOptions - */ - protected $moduleOptions; - - /** - * @var AuthorizationInterface - */ - protected $authorization; - - /** - * @var RouteResult - */ - protected $routeResult; - - /** - * @var array - */ - protected $isActiveCache = []; - - /** - * @var array - */ - protected $hrefCache = []; - - /** - * @var bool - */ - protected $isActiveRecursion = true; + protected FactoryInterface $providerFactory; + protected RouteHelper $routeHelper; + protected NavigationOptions $moduleOptions; + protected ?AuthorizationInterface $authorization; + protected ?RouteResult $routeResult = null; + protected array $containers = []; + protected array $isActiveCache = []; + protected array $hrefCache = []; + protected bool $isActiveRecursion = true; - /** - * NavigationService constructor. - * @param Factory $providerFactory - * @param AuthorizationInterface $authorization - * @param RouteHelper $routeHelper - * @param NavigationOptions $moduleOptions - */ public function __construct( - Factory $providerFactory, + FactoryInterface $providerFactory, RouteHelper $routeHelper, NavigationOptions $moduleOptions, - AuthorizationInterface $authorization = null + ?AuthorizationInterface $authorization = null ) { - $this->routeHelper = $routeHelper; - $this->authorization = $authorization; + $this->routeHelper = $routeHelper; + $this->authorization = $authorization; $this->providerFactory = $providerFactory; - $this->moduleOptions = $moduleOptions; + $this->moduleOptions = $moduleOptions; } - /** - * @return RouteResult - */ - public function getRouteResult(): RouteResult + public function getRouteResult(): ?RouteResult { return $this->routeResult; } - /** - * @param RouteResult $routeResult - */ - public function setRouteResult(RouteResult $routeResult) + public function setRouteResult(RouteResult $routeResult): void { $this->routeResult = $routeResult; } - /** - * @return bool - */ public function getIsActiveRecursion(): bool { return $this->isActiveRecursion; } - /** - * @param $isActiveRecursion - */ - public function setIsActiveRecursion(bool $isActiveRecursion) + public function setIsActiveRecursion(bool $isActiveRecursion): void { - if ($isActiveRecursion != $this->isActiveRecursion) { + if ($isActiveRecursion !== $this->isActiveRecursion) { $this->isActiveRecursion = $isActiveRecursion; - $this->isActiveCache = array(); + $this->isActiveCache = []; } } - /** - * @param string $name - * @return NavigationContainer - */ + public function getIsActiveCache(): array + { + return $this->isActiveCache; + } + + public function getHrefCache(): array + { + return $this->hrefCache; + } + public function getContainer(string $name): NavigationContainer { if (isset($this->containers[$name])) { @@ -133,37 +83,21 @@ public function getContainer(string $name): NavigationContainer } $containersConfig = $this->moduleOptions->getContainers(); - $containerConfig = $containersConfig[$name] ?? []; + $containerConfig = $containersConfig[$name] ?? []; if (empty($containerConfig)) { throw new RuntimeException(sprintf('Container `%s` is not defined', $name)); } - /** @var ProviderInterface $containerProvider */ - $containerProvider = $this->providerFactory->create($containerConfig); - - $container = $containerProvider->getContainer(); - if (!$container instanceof NavigationContainer) { - throw new RuntimeException( - sprintf( - "Navigation container for name %s is not an instance of %s", - $name, - NavigationContainer::class - ) - ); - } + $containerProvider = $this->providerFactory->create($containerConfig); + $this->containers[$name] = $containerProvider->getContainer(); - $this->containers[$name] = $container; return $this->containers[$name]; } - /** - * @param Page $page - * @return bool - */ public function isAllowed(Page $page): bool { //authorization module is optional, this function will always return true if missing - if (!$this->authorization) { + if (! $this->authorization instanceof AuthorizationInterface) { return true; } @@ -179,35 +113,27 @@ public function isAllowed(Page $page): bool return true; } - /** - * @param Page $page - * @return bool - */ public function isActive(Page $page): bool { $hash = spl_object_hash($page); if (isset($this->isActiveCache[$hash])) { return $this->isActiveCache[$hash]; } + $active = false; - if ($this->routeResult && $this->routeResult->isSuccess()) { + if ($this->routeResult instanceof RouteResult && $this->routeResult->isSuccess()) { $routeName = $this->routeResult->getMatchedRouteName(); $pageRoute = $page->getOption('route'); if ($pageRoute) { if ($pageRoute['route_name'] === $routeName) { - $reqParams = array_merge($this->routeResult->getMatchedParams(), $_GET); - $pageParams = array_merge( - $pageRoute['route_params'] ?? [], - $pageRoute['query_params'] ?? [] - ); + $reqParams = array_merge($this->routeResult->getMatchedParams(), $_GET); + $pageParams = array_merge($pageRoute['route_params'] ?? [], $pageRoute['query_params'] ?? []); - $ignoreParams = $pageRoute['ignore_params'] ?? []; - $active = $this->areParamsEqual($pageParams, $reqParams, $ignoreParams); + $active = $this->areParamsEqual($pageParams, $reqParams, $pageRoute['ignore_params'] ?? []); } elseif ($this->isActiveRecursion) { - $iterator = new \RecursiveIteratorIterator($page, \RecursiveIteratorIterator::CHILD_FIRST); - /** @var Page $page */ + $iterator = new RecursiveIteratorIterator($page, RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $leaf) { - if (!$leaf instanceof Page) { + if (! $leaf instanceof Page) { continue; } if ($this->isActive($leaf)) { @@ -222,12 +148,6 @@ public function isActive(Page $page): bool return $active; } - /** - * @param array $pageParams - * @param array $requestParams - * @param array $ignoreParams - * @return bool - */ protected function areParamsEqual(array $pageParams, array $requestParams, array $ignoreParams): bool { foreach ($ignoreParams as $unsetKey) { @@ -239,10 +159,6 @@ protected function areParamsEqual(array $pageParams, array $requestParams, array return empty($diff); } - /** - * @param Page $page - * @return string - */ public function getHref(Page $page): string { $hash = spl_object_hash($page); @@ -255,7 +171,7 @@ public function getHref(Page $page): string $href = $page->getOption('uri'); } elseif ($page->getOption('route')) { $pageRoute = $page->getOption('route'); - $href = $this->routeHelper->generateUri($pageRoute); + $href = $this->routeHelper->generateUri($pageRoute); } if ($href) { diff --git a/src/Service/NavigationInterface.php b/src/Service/NavigationInterface.php index 64c5a22..001dd02 100644 --- a/src/Service/NavigationInterface.php +++ b/src/Service/NavigationInterface.php @@ -1,11 +1,6 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\Service; @@ -13,43 +8,25 @@ use Dot\Navigation\Page; use Mezzio\Router\RouteResult; -/** - * Interface NavigationInterface - * @package Dot\Navigation\Service - */ interface NavigationInterface { - /** - * @param RouteResult $routeResult - */ + public function getRouteResult(): ?RouteResult; + public function setRouteResult(RouteResult $routeResult); - /** - * @param $isActiveRecursion - */ + public function getIsActiveRecursion(): bool; + public function setIsActiveRecursion(bool $isActiveRecursion); - /** - * @param $name - * @return NavigationContainer - */ + public function getIsActiveCache(): array; + + public function getHrefCache(): array; + public function getContainer(string $name): NavigationContainer; - /** - * @param Page $page - * @return bool - */ public function isAllowed(Page $page): bool; - /** - * @param Page $page - * @return bool - */ public function isActive(Page $page): bool; - /** - * @param Page $page - * @return string - */ public function getHref(Page $page): string; } diff --git a/src/View/AbstractNavigationRenderer.php b/src/View/AbstractNavigationRenderer.php index 8e78aa5..8f14c4d 100644 --- a/src/View/AbstractNavigationRenderer.php +++ b/src/View/AbstractNavigationRenderer.php @@ -1,11 +1,6 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\View; @@ -15,47 +10,44 @@ use Laminas\Escaper\Escaper; use Mezzio\Template\TemplateRendererInterface; -/** - * Class AbstractNavigationHelper - * @package Dot\Navigation\Helper - */ +use function implode; +use function in_array; +use function is_array; +use function is_scalar; +use function is_string; +use function json_encode; +use function preg_match; +use function str_contains; +use function str_ends_with; +use function str_replace; +use function str_starts_with; +use function strlen; +use function substr; +use function trim; + abstract class AbstractNavigationRenderer implements RendererInterface { - /** @var NavigationInterface */ - protected $navigation; - - /** @var string */ - protected $partial; - - /** @var TemplateRendererInterface */ - protected $template; + protected NavigationInterface $navigation; + protected TemplateRendererInterface $template; + protected string $partial; - /** - * AbstractNavigationHelper constructor. - * @param NavigationInterface $navigation - * @param TemplateRendererInterface $template - */ public function __construct(NavigationInterface $navigation, TemplateRendererInterface $template) { $this->navigation = $navigation; - $this->template = $template; + $this->template = $template; } - /** - * @param array $attributes - * @return string - */ public function htmlAttributes(array $attributes): string { - $xhtml = ''; + $xhtml = ''; $escaper = new Escaper(); foreach ($attributes as $key => $val) { $key = $escaper->escapeHtml($key); - if (('on' == substr($key, 0, 2)) || ('constraints' == $key)) { + if ((str_starts_with($key, 'on')) || ('constraints' === $key)) { // Don't escape event attributes; _do_ substitute double quotes with singles - if (!is_scalar($val)) { + if (! is_scalar($val)) { // non-scalar data should be cast to JSON first $val = json_encode($val); } @@ -67,10 +59,10 @@ public function htmlAttributes(array $attributes): string $val = $escaper->escapeHtmlAttr($val); - if ('id' == $key) { + if ('id' === $key) { $val = $this->normalizeId($val); } - if (strpos($val, '"') !== false) { + if (str_contains($val, '"')) { $xhtml .= " $key='$val'"; } else { $xhtml .= " $key=\"$val\""; @@ -79,16 +71,10 @@ public function htmlAttributes(array $attributes): string return $xhtml; } - /** - * Normalize an ID - * - * @param string $value - * @return string - */ protected function normalizeId(string $value): string { - if (strstr($value, '[')) { - if ('[]' == substr($value, -2)) { + if (str_contains($value, '[')) { + if (str_ends_with($value, '[]')) { $value = substr($value, 0, strlen($value) - 2); } $value = trim($value, ']'); @@ -98,44 +84,27 @@ protected function normalizeId(string $value): string return $value; } - /** - * @return string|null - */ public function getPartial(): ?string { return $this->partial; } - /** - * @param string $partial - */ - public function setPartial(string $partial) + public function setPartial(string $partial): void { $this->partial = $partial; } - /** - * @param string|NavigationContainer $container - * @return NavigationContainer - */ - protected function getContainer($container): NavigationContainer + protected function getContainer(string|NavigationContainer $container): NavigationContainer { if (is_string($container)) { return $this->navigation->getContainer($container); - } elseif (!$container instanceof NavigationContainer) { + } elseif (! $container instanceof NavigationContainer) { throw new RuntimeException('Container must be a string or an instance of ' . NavigationContainer::class); } return $container; } - /** - * Cleans array of attributes based on valid input. - * - * @param array $input - * @param array $valid - * @return array - */ protected function cleanAttributes(array $input, array $valid): array { foreach ($input as $key => $value) { diff --git a/src/View/NavigationRenderer.php b/src/View/NavigationRenderer.php index 01bcdd9..3232512 100644 --- a/src/View/NavigationRenderer.php +++ b/src/View/NavigationRenderer.php @@ -1,38 +1,22 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\View; use Dot\Navigation\NavigationContainer; use Dot\Navigation\Options\NavigationOptions; -use Dot\Navigation\Service\Navigation; +use Dot\Navigation\Service\NavigationInterface; use Mezzio\Template\TemplateRendererInterface; -/** - * Class NavigationRenderer - * @package Dot\Navigation\View - */ +use function array_merge; + class NavigationRenderer extends AbstractNavigationRenderer { - /** - * @var NavigationOptions - */ - protected $options; + protected NavigationOptions $options; - /** - * NavigationMenu constructor. - * @param Navigation $navigation - * @param TemplateRendererInterface $template - * @param NavigationOptions $options - */ public function __construct( - Navigation $navigation, + NavigationInterface $navigation, TemplateRendererInterface $template, NavigationOptions $options ) { @@ -40,13 +24,7 @@ public function __construct( parent::__construct($navigation, $template); } - /** - * @param string $partial - * @param string|NavigationContainer $container - * @param array $params - * @return string - */ - public function renderPartial($container, string $partial, array $params = []): string + public function renderPartial(string|NavigationContainer $container, string $partial, array $params = []): string { $container = $this->getContainer($container); @@ -59,13 +37,16 @@ public function renderPartial($container, string $partial, array $params = []): ); } - /** - * @param string|NavigationContainer $container - * @return string - */ - public function render($container): string + public function render(string|NavigationContainer $container, string $template, array $params = []): string { - // TODO: render a default HTML menu structure - return ''; + $container = $this->getContainer($container); + + return $this->template->render( + $template, + array_merge( + ['container' => $container, 'navigation' => $this->navigation], + $params + ) + ); } } diff --git a/src/View/RendererInterface.php b/src/View/RendererInterface.php index 73588fa..f686463 100644 --- a/src/View/RendererInterface.php +++ b/src/View/RendererInterface.php @@ -1,39 +1,16 @@ <?php -/** - * @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository - * @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com) - * @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License - */ -declare(strict_types = 1); +declare(strict_types=1); namespace Dot\Navigation\View; use Dot\Navigation\NavigationContainer; -/** - * Interface RendererInterface - * @package Dot\Navigation\View - */ interface RendererInterface { - /** - * @param string|NavigationContainer $container - * @return string - */ - public function render($container): string; + public function render(string|NavigationContainer $container, string $template, array $params = []): string; - /** - * @param $partial - * @param array $params - * @param string|NavigationContainer $container - * @return string - */ - public function renderPartial($container, string $partial, array $params = []): string; + public function renderPartial(string|NavigationContainer $container, string $partial, array $params = []): string; - /** - * @param array $attributes - * @return string - */ public function htmlAttributes(array $attributes): string; } diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php new file mode 100644 index 0000000..186347d --- /dev/null +++ b/test/ConfigProviderTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation; + +use Dot\Navigation\ConfigProvider; +use Dot\Navigation\Factory\NavigationMiddlewareFactory; +use Dot\Navigation\Factory\NavigationOptionsFactory; +use Dot\Navigation\Factory\NavigationRendererFactory; +use Dot\Navigation\Factory\NavigationServiceFactory; +use Dot\Navigation\Factory\ProviderPluginManagerFactory; +use Dot\Navigation\NavigationMiddleware; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Provider\Factory; +use Dot\Navigation\Provider\FactoryInterface; +use Dot\Navigation\Provider\ProviderPluginManager; +use Dot\Navigation\Service\Navigation; +use Dot\Navigation\Service\NavigationInterface; +use Dot\Navigation\View\NavigationRenderer; +use Dot\Navigation\View\RendererInterface; +use PHPUnit\Framework\TestCase; + +class ConfigProviderTest extends TestCase +{ + protected array $config; + + protected function setup(): void + { + $this->config = (new ConfigProvider())(); + } + + public function testHasDependencies(): void + { + $this->assertArrayHasKey('dependencies', $this->config); + } + + public function testDependenciesHasFactories(): void + { + $this->assertArrayHasKey('factories', $this->config['dependencies']); + + $factories = $this->config['dependencies']['factories']; + $this->assertArrayHasKey(Navigation::class, $factories); + $this->assertSame(NavigationServiceFactory::class, $factories[Navigation::class]); + $this->assertArrayHasKey(NavigationMiddleware::class, $factories); + $this->assertSame(NavigationMiddlewareFactory::class, $factories[NavigationMiddleware::class]); + $this->assertArrayHasKey(NavigationOptions::class, $factories); + $this->assertSame(NavigationOptionsFactory::class, $factories[NavigationOptions::class]); + $this->assertArrayHasKey(NavigationRenderer::class, $factories); + $this->assertSame(NavigationRendererFactory::class, $factories[NavigationRenderer::class]); + $this->assertArrayHasKey(ProviderPluginManager::class, $factories); + $this->assertSame(ProviderPluginManagerFactory::class, $factories[ProviderPluginManager::class]); + } + + public function testDependenciesHasAliases(): void + { + $this->assertArrayHasKey('aliases', $this->config['dependencies']); + + $aliases = $this->config['dependencies']['aliases']; + $this->assertArrayHasKey(FactoryInterface::class, $aliases); + $this->assertSame(Factory::class, $aliases[FactoryInterface::class]); + $this->assertArrayHasKey(NavigationInterface::class, $aliases); + $this->assertSame(Navigation::class, $aliases[NavigationInterface::class]); + $this->assertArrayHasKey(RendererInterface::class, $aliases); + $this->assertSame(NavigationRenderer::class, $aliases[RendererInterface::class]); + } +} diff --git a/test/Exception/InvalidArgumentExceptionTest.php b/test/Exception/InvalidArgumentExceptionTest.php new file mode 100644 index 0000000..d283fd9 --- /dev/null +++ b/test/Exception/InvalidArgumentExceptionTest.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Exception; + +use Dot\Navigation\Exception\ExceptionInterface; +use Dot\Navigation\Exception\InvalidArgumentException; +use PHPUnit\Framework\TestCase; + +class InvalidArgumentExceptionTest extends TestCase +{ + public function testWillReturnCorrectInstances(): void + { + $exception = new InvalidArgumentException('test'); + $this->assertInstanceOf(InvalidArgumentException::class, $exception); + $this->assertInstanceOf(ExceptionInterface::class, $exception); + } +} diff --git a/test/Exception/RuntimeExceptionTest.php b/test/Exception/RuntimeExceptionTest.php new file mode 100644 index 0000000..9d46320 --- /dev/null +++ b/test/Exception/RuntimeExceptionTest.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Exception; + +use Dot\Navigation\Exception\ExceptionInterface; +use Dot\Navigation\Exception\RuntimeException; +use PHPUnit\Framework\TestCase; + +class RuntimeExceptionTest extends TestCase +{ + public function setUp(): void + { + parent::setUp(); + } + + public function testWillReturnCorrectInstances(): void + { + $exception = new RuntimeException('test'); + $this->assertInstanceOf(RuntimeException::class, $exception); + $this->assertInstanceOf(ExceptionInterface::class, $exception); + } +} diff --git a/test/Factory/NavigationMiddlewareFactoryTest.php b/test/Factory/NavigationMiddlewareFactoryTest.php new file mode 100644 index 0000000..d075b39 --- /dev/null +++ b/test/Factory/NavigationMiddlewareFactoryTest.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Factory; + +use Dot\Navigation\Factory\NavigationMiddlewareFactory; +use Dot\Navigation\NavigationMiddleware; +use Dot\Navigation\Service\NavigationInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class NavigationMiddlewareFactoryTest extends TestCase +{ + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateMiddlewareWithoutNavigationInterface(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with(NavigationInterface::class) + ->willReturn(false); + + $this->expectExceptionMessage(NavigationMiddlewareFactory::MESSAGE_MISSING_NAVIGATION); + (new NavigationMiddlewareFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillCreateMiddleware(): void + { + $container = $this->createMock(ContainerInterface::class); + $navigation = $this->createMock(NavigationInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with(NavigationInterface::class) + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with(NavigationInterface::class) + ->willReturn($navigation); + + $middleware = (new NavigationMiddlewareFactory())($container); + $this->assertInstanceOf(NavigationMiddleware::class, $middleware); + } +} diff --git a/test/Factory/NavigationOptionsFactoryTest.php b/test/Factory/NavigationOptionsFactoryTest.php new file mode 100644 index 0000000..6455d8d --- /dev/null +++ b/test/Factory/NavigationOptionsFactoryTest.php @@ -0,0 +1,84 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Factory; + +use Dot\Navigation\Factory\NavigationOptionsFactory; +use Dot\Navigation\Options\NavigationOptions; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class NavigationOptionsFactoryTest extends TestCase +{ + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationOptionsWithoutConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectExceptionMessage(NavigationOptionsFactory::MESSAGE_MISSING_CONFIG); + (new NavigationOptionsFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationOptionsWithoutPackageConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([]); + + $this->expectExceptionMessage(NavigationOptionsFactory::MESSAGE_MISSING_PACKAGE_CONFIG); + (new NavigationOptionsFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillCreateNavigationOptions(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'dot_navigation' => [ + 'active_recursion' => true, + ], + ]); + + $options = (new NavigationOptionsFactory())($container); + $this->assertInstanceOf(NavigationOptions::class, $options); + } +} diff --git a/test/Factory/NavigationRendererFactoryTest.php b/test/Factory/NavigationRendererFactoryTest.php new file mode 100644 index 0000000..38e91ff --- /dev/null +++ b/test/Factory/NavigationRendererFactoryTest.php @@ -0,0 +1,102 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Factory; + +use Dot\Navigation\Factory\NavigationRendererFactory; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Service\NavigationInterface; +use Dot\Navigation\View\NavigationRenderer; +use Mezzio\Template\TemplateRendererInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class NavigationRendererFactoryTest extends TestCase +{ + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationRendererWithoutNavigationInterface(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with(NavigationInterface::class) + ->willReturn(false); + + $this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_NAVIGATION_INTERFACE); + (new NavigationRendererFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationRendererWithoutTemplateRendererInterface(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturnMap([ + [NavigationInterface::class, true], + [TemplateRendererInterface::class, false], + ]); + + $this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_TEMPLATE_RENDERER); + (new NavigationRendererFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationRendererWithoutNavigationOptions(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturnMap([ + [NavigationInterface::class, true], + [TemplateRendererInterface::class, true], + [NavigationOptions::class, false], + ]); + + $this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_NAVIGATION_OPTIONS); + (new NavigationRendererFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationRenderer(): void + { + $container = $this->createMock(ContainerInterface::class); + $navigation = $this->createMock(NavigationInterface::class); + $renderer = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + + $container->method('has')->willReturnMap([ + [NavigationInterface::class, true], + [TemplateRendererInterface::class, true], + [NavigationOptions::class, true], + ]); + + $container->method('get')->willReturnMap([ + [NavigationInterface::class, $navigation], + [TemplateRendererInterface::class, $renderer], + [NavigationOptions::class, $options], + ]); + + $renderer = (new NavigationRendererFactory())($container); + $this->assertInstanceOf(NavigationRenderer::class, $renderer); + } +} diff --git a/test/Factory/NavigationServiceFactoryTest.php b/test/Factory/NavigationServiceFactoryTest.php new file mode 100644 index 0000000..fbe9326 --- /dev/null +++ b/test/Factory/NavigationServiceFactoryTest.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Factory; + +use Dot\Authorization\AuthorizationInterface; +use Dot\Helpers\Route\RouteHelper; +use Dot\Navigation\Factory\NavigationServiceFactory; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Provider\ProviderPluginManager; +use Dot\Navigation\Service\Navigation; +use Mezzio\Template\TemplateRendererInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class NavigationServiceFactoryTest extends TestCase +{ + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationServiceWithoutRouteHelper(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with(RouteHelper::class) + ->willReturn(false); + + $this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_ROUTE_HELPER); + (new NavigationServiceFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationServiceWithoutProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturnMap([ + [RouteHelper::class, true], + [ProviderPluginManager::class, false], + ]); + + $this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_PLUGIN_MANAGER); + (new NavigationServiceFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateNavigationServiceWithoutNavigationOptions(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturnMap([ + [RouteHelper::class, true], + [ProviderPluginManager::class, true], + [NavigationOptions::class, false], + ]); + + $this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_NAVIGATION_OPTIONS); + (new NavigationServiceFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillCreateNavigationServiceWithoutAuthorizationInterface(): void + { + $container = $this->createMock(ContainerInterface::class); + $navigation = $this->createMock(RouteHelper::class); + $renderer = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + + $container->method('has')->willReturnMap([ + [RouteHelper::class, true], + [ProviderPluginManager::class, true], + [NavigationOptions::class, true], + [AuthorizationInterface::class, false], + ]); + + $container->method('get')->willReturnMap([ + [RouteHelper::class, $navigation], + [TemplateRendererInterface::class, $renderer], + [NavigationOptions::class, $options], + [AuthorizationInterface::class, null], + ]); + + $service = (new NavigationServiceFactory())($container); + $this->assertInstanceOf(Navigation::class, $service); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillCreateNavigationServiceWithAuthorizationInterface(): void + { + $container = $this->createMock(ContainerInterface::class); + $navigation = $this->createMock(RouteHelper::class); + $renderer = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + $authorization = $this->createMock(AuthorizationInterface::class); + + $container->method('has')->willReturnMap([ + [RouteHelper::class, true], + [ProviderPluginManager::class, true], + [NavigationOptions::class, true], + [AuthorizationInterface::class, true], + ]); + + $container->method('get')->willReturnMap([ + [RouteHelper::class, $navigation], + [TemplateRendererInterface::class, $renderer], + [NavigationOptions::class, $options], + [AuthorizationInterface::class, $authorization], + ]); + + $service = (new NavigationServiceFactory())($container); + $this->assertInstanceOf(Navigation::class, $service); + } +} diff --git a/test/Factory/ProviderPluginManagerFactoryTest.php b/test/Factory/ProviderPluginManagerFactoryTest.php new file mode 100644 index 0000000..e79cfa5 --- /dev/null +++ b/test/Factory/ProviderPluginManagerFactoryTest.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Factory; + +use Dot\Navigation\Factory\ProviderPluginManagerFactory; +use Dot\Navigation\Provider\ProviderPluginManager; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class ProviderPluginManagerFactoryTest extends TestCase +{ + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateProviderPluginManagerWithoutConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_CONFIG); + (new ProviderPluginManagerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateProviderPluginManagerWithoutPackageConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([]); + + $this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_PACKAGE_CONFIG); + (new ProviderPluginManagerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateProviderPluginManagerWithoutConfigProviderManager(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'dot_navigation' => [ + 'active_recursion' => true, + ], + ]); + + $this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER); + (new ProviderPluginManagerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillCreateProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'dot_navigation' => [ + 'provider_manager' => [], + ], + ]); + + $manager = (new ProviderPluginManagerFactory())($container); + $this->assertInstanceOf(ProviderPluginManager::class, $manager); + } +} diff --git a/test/Filter/IsAllowedFilterTest.php b/test/Filter/IsAllowedFilterTest.php new file mode 100644 index 0000000..6f9f3ed --- /dev/null +++ b/test/Filter/IsAllowedFilterTest.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Filter; + +use Dot\Authorization\AuthorizationInterface; +use Dot\Helpers\Route\RouteHelper; +use Dot\Navigation\Filter\IsAllowedFilter; +use Dot\Navigation\NavigationContainer; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Page; +use Dot\Navigation\Provider\FactoryInterface; +use Dot\Navigation\Service\Navigation; +use Dot\Navigation\Service\NavigationInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use RecursiveIterator; + +class IsAllowedFilterTest extends TestCase +{ + /** + * @throws Exception + */ + public function testWillAcceptWithoutAuthorization(): void + { + $iterator = $this->createMock(RecursiveIterator::class); + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options, null); + $filter = $this->getMockBuilder(IsAllowedFilter::class) + ->setConstructorArgs([$iterator, $navigation]) + ->onlyMethods(['current']) + ->getMock(); + $filter->method('current')->willReturn(new Page()); + + $this->assertTrue($filter->accept()); + } + + /** + * @throws Exception + */ + public function testWillAcceptWithAuthorization(): void + { + $iterator = $this->createMock(RecursiveIterator::class); + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $authorization = $this->createMock(AuthorizationInterface::class); + + $authorization->expects($this->once())->method('isGranted')->willReturn(true); + + $page = new Page(); + $page->setOption('permission', 'test'); + $page->setOption('roles', []); + + $navigation = new Navigation($factory, $route, $options, $authorization); + $filter = $this->getMockBuilder(IsAllowedFilter::class) + ->setConstructorArgs([$iterator, $navigation]) + ->onlyMethods(['current']) + ->getMock(); + $filter->method('current')->willReturn($page); + + $this->assertTrue($filter->accept()); + } + + /** + * @throws Exception + */ + public function testGetChildren(): void + { + $navigation = $this->createMock(NavigationInterface::class); + + $filter = new IsAllowedFilter(new NavigationContainer([new Page()]), $navigation); + $this->assertInstanceOf(IsAllowedFilter::class, $filter->getChildren()); + } + + /** + * @throws Exception + */ + public function testWillCreateFilter(): void + { + $iterator = $this->createMock(RecursiveIterator::class); + $navigation = $this->createMock(NavigationInterface::class); + + $filter = new IsAllowedFilter($iterator, $navigation); + $this->assertInstanceOf(IsAllowedFilter::class, $filter); + } +} diff --git a/test/NavigationContainerTest.php b/test/NavigationContainerTest.php new file mode 100644 index 0000000..1b3945d --- /dev/null +++ b/test/NavigationContainerTest.php @@ -0,0 +1,213 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation; + +use Dot\Navigation\NavigationContainer; +use Dot\Navigation\Page; +use PHPUnit\Framework\TestCase; + +class NavigationContainerTest extends TestCase +{ + protected int $count = 5; + + public function testWillAddPages(): void + { + $navigationContainer = new NavigationContainer(); + $this->assertFalse($navigationContainer->hasChildren()); + $navigationContainer->addPages([ + new Page(), + ]); + $this->assertTrue($navigationContainer->hasChildren()); + } + + public function testWillAddPage(): void + { + $navigationContainer = new NavigationContainer(); + $this->assertFalse($navigationContainer->hasChildren()); + $navigationContainer->addPage(new Page()); + $this->assertTrue($navigationContainer->hasChildren()); + } + + public function testWillReturnCurrentChild(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $currentPage = $navigationContainer->current(); + $this->assertInstanceOf(Page::class, $currentPage); + $this->assertSame($pages[0]->getOption('opt'), $currentPage->getOption('opt')); + } + + public function testWillReturnNextChild(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + $navigationContainer->next(); + + $currentPage = $navigationContainer->current(); + $this->assertInstanceOf(Page::class, $currentPage); + $this->assertSame($pages[1]->getOption('opt'), $currentPage->getOption('opt')); + } + + public function testWillReturnCurrentKey(): void + { + $navigationContainer = new NavigationContainer(); + $this->assertSame(0, $navigationContainer->key()); + } + + public function testWillReturnInvalidIfNoItems(): void + { + $navigationContainer = new NavigationContainer(); + $this->assertFalse($navigationContainer->valid()); + } + + public function testWillReturnValidWhileIndexExists(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $this->assertTrue($navigationContainer->valid()); + for ($i = 0; $i < $this->count; $i++) { + $this->assertTrue($navigationContainer->valid()); + $navigationContainer->next(); + } + $this->assertFalse($navigationContainer->valid()); + } + + public function testWillReturnFalseIfNotHasChildren(): void + { + $navigationContainer = new NavigationContainer(); + $this->assertFalse($navigationContainer->hasChildren()); + } + + public function testWillReturnTrueIfHasChildren(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $this->assertTrue($navigationContainer->hasChildren()); + } + + public function testWillNotFindOneByNotExistingAttribute(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $result = $navigationContainer->findOneByAttribute('test', 'test'); + $this->assertNull($result); + } + + public function testWillFindOneByExistingAttribute(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $result = $navigationContainer->findOneByAttribute('attr', 'attr #0'); + $this->assertInstanceOf(Page::class, $result); + $this->assertSame($pages[0]->getAttribute('attr'), $result->getAttribute('attr')); + } + + public function testWillNotFindManyByNonExistingAttribute(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $results = $navigationContainer->findByAttribute('test', 'test'); + $this->assertIsArray($results); + $this->assertEmpty($results); + } + + public function testWillFindManyByExistingAttribute(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $results = $navigationContainer->findByAttribute('attr', 'attr #0'); + $this->assertIsArray($results); + $this->assertInstanceOf(Page::class, $results[0]); + $this->assertSame($pages[0]->getAttribute('attr'), $results[0]->getAttribute('attr')); + } + + public function testWillNotFindOneByNonExistingOption(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $result = $navigationContainer->findOneByOption('test', 'test'); + $this->assertNull($result); + } + + public function testWillFindOneByExistingOption(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $result = $navigationContainer->findOneByOption('opt', 'opt #0'); + $this->assertInstanceOf(Page::class, $result); + $this->assertSame($pages[0]->getOption('opt'), $result->getOption('opt')); + } + + public function testWillNotFindManyByNonExistingOption(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $results = $navigationContainer->findByOption('test', 'test'); + $this->assertIsArray($results); + $this->assertEmpty($results); + } + + public function testWillFindManyByExistingOption(): void + { + $pages = $this->getTestPages(); + + $navigationContainer = new NavigationContainer(); + $navigationContainer->addPages($pages); + + $results = $navigationContainer->findByOption('opt', 'opt #0'); + $this->assertIsArray($results); + $this->assertInstanceOf(Page::class, $results[0]); + $this->assertSame($pages[0]->getAttribute('opt'), $results[0]->getAttribute('opt')); + } + + /** + * @return Page[] + */ + protected function getTestPages(): array + { + $pages = []; + + for ($i = 0; $i < $this->count; $i++) { + $page = new Page(); + $page->setOption('opt', 'opt #' . $i); + $page->setAttribute('attr', 'attr #' . $i); + $pages[] = $page; + } + + return $pages; + } +} diff --git a/test/NavigationMiddlewareTest.php b/test/NavigationMiddlewareTest.php new file mode 100644 index 0000000..2063f7a --- /dev/null +++ b/test/NavigationMiddlewareTest.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation; + +use Dot\Navigation\NavigationMiddleware; +use Dot\Navigation\Service\NavigationInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class NavigationMiddlewareTest extends TestCase +{ + /** + * @throws Exception + */ + public function testWillCreateMiddleware(): void + { + $navigation = $this->createMock(NavigationInterface::class); + + $middleware = new NavigationMiddleware($navigation); + $this->assertInstanceOf(NavigationMiddleware::class, $middleware); + } + + /** + * @throws Exception + */ + public function testWillProcessRequest(): void + { + $navigation = $this->createMock(NavigationInterface::class); + $request = $this->createMock(ServerRequestInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); + + $middleware = new NavigationMiddleware($navigation); + $response = $middleware->process($request, $handler); + $this->assertInstanceOf(ResponseInterface::class, $response); + } +} diff --git a/test/Options/NavigationOptionsTest.php b/test/Options/NavigationOptionsTest.php new file mode 100644 index 0000000..dfe4f2d --- /dev/null +++ b/test/Options/NavigationOptionsTest.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Options; + +use Dot\Navigation\Options\NavigationOptions; +use PHPUnit\Framework\TestCase; + +class NavigationOptionsTest extends TestCase +{ + public function testWillCreateNavigationOptions(): void + { + $options = new NavigationOptions(); + $this->assertInstanceOf(NavigationOptions::class, $options); + } + + public function testAccessors(): void + { + $options = new NavigationOptions(); + $this->assertIsArray($options->getContainers()); + $this->assertEmpty($options->getContainers()); + $options->setContainers(['test']); + $this->assertIsArray($options->getContainers()); + $this->assertCount(1, $options->getContainers()); + $this->assertTrue($options->getActiveRecursion()); + $options->setActiveRecursion(false); + $this->assertFalse($options->getActiveRecursion()); + } +} diff --git a/test/PageTest.php b/test/PageTest.php new file mode 100644 index 0000000..cc6601e --- /dev/null +++ b/test/PageTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation; + +use Dot\Navigation\Page; +use PHPUnit\Framework\TestCase; + +class PageTest extends TestCase +{ + public function testWillCreatePage(): void + { + $page = new Page(); + $this->assertInstanceOf(Page::class, $page); + } + + public function testParentAccessors(): void + { + $page = new Page(); + $this->assertFalse($page->hasParent()); + $page->setParent(new Page()); + $this->assertTrue($page->hasParent()); + $this->assertInstanceOf(Page::class, $page->getParent()); + } + + public function testWillAddPage(): void + { + $page1 = new Page(); + $page2 = new Page(); + $this->assertFalse($page1->hasChildren()); + $this->assertFalse($page2->hasParent()); + $page1->addPage($page2); + $this->assertTrue($page1->hasChildren()); + $this->assertTrue($page2->hasParent()); + } + + public function testOptionAccessors(): void + { + $page = new Page(); + $this->assertIsArray($page->getOptions()); + $this->assertEmpty($page->getOptions()); + $this->assertFalse($page->hasOptions()); + $page->setOption('opt1', 'value1'); + $this->assertIsArray($page->getOptions()); + $this->assertCount(1, $page->getOptions()); + $this->assertTrue($page->hasOptions()); + $this->assertSame('value1', $page->getOption('opt1')); + $page->setOptions([ + 'opt1' => 'value1', + 'opt2' => 'value2', + ]); + $this->assertIsArray($page->getOptions()); + $this->assertCount(2, $page->getOptions()); + $this->assertTrue($page->hasOptions()); + } + + public function testAttributeAccessors(): void + { + $page = new Page(); + $this->assertIsArray($page->getAttributes()); + $this->assertEmpty($page->getAttributes()); + $this->assertFalse($page->hasAttributes()); + $page->setAttribute('attr1', 'value1'); + $this->assertIsArray($page->getAttributes()); + $this->assertCount(1, $page->getAttributes()); + $this->assertTrue($page->hasAttributes()); + $this->assertSame('value1', $page->getAttribute('attr1')); + $page->setAttributes([ + 'attr1' => 'value1', + 'attr2' => 'value2', + ]); + $this->assertIsArray($page->getAttributes()); + $this->assertCount(2, $page->getAttributes()); + $this->assertTrue($page->hasAttributes()); + } + + public function testGetLabel(): void + { + $page = new Page(); + $this->assertSame('Not defined', $page->getLabel()); + $page->setOption('label', 'Label'); + $this->assertSame('Label', $page->getLabel()); + } +} diff --git a/test/Provider/ArrayProviderTest.php b/test/Provider/ArrayProviderTest.php new file mode 100644 index 0000000..329194e --- /dev/null +++ b/test/Provider/ArrayProviderTest.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Provider; + +use Dot\Navigation\NavigationContainer; +use Dot\Navigation\Provider\ArrayProvider; +use PHPUnit\Framework\TestCase; + +class ArrayProviderTest extends TestCase +{ + public function testWillCreateArrayProvider(): void + { + $provider = new ArrayProvider(); + $this->assertInstanceOf(ArrayProvider::class, $provider); + } + + public function testAccessors(): void + { + $provider = new ArrayProvider(); + $this->assertFalse($provider->hasItems()); + $provider->setItems(['test']); + $this->assertTrue($provider->hasItems()); + $this->assertSame(['test'], $provider->getItems()); + } + + public function testGetContainer(): void + { + $pageSpecs = [ + [ + 'pages' => [ + ['attributes' => ['attr' => 'attribute #0'], 'options' => ['opt' => 'option #0']], + ['attributes' => ['attr' => 'attribute #1'], 'options' => ['opt' => 'option #1']], + ], + ], + ]; + + $provider = new ArrayProvider([ + 'items' => $pageSpecs, + ]); + $container = $provider->getContainer(); + $this->assertInstanceOf(NavigationContainer::class, $container); + $this->assertCount(2, $container->getChildren()); + } +} diff --git a/test/Provider/FactoryTest.php b/test/Provider/FactoryTest.php new file mode 100644 index 0000000..1570f7e --- /dev/null +++ b/test/Provider/FactoryTest.php @@ -0,0 +1,128 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Provider; + +use Dot\Navigation\Exception\RuntimeException; +use Dot\Navigation\Provider\ArrayProvider; +use Dot\Navigation\Provider\Factory; +use Dot\Navigation\Provider\ProviderInterface; +use Dot\Navigation\Provider\ProviderPluginManager; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; + +use function sprintf; + +class FactoryTest extends TestCase +{ + /** + * @throws Exception + */ + public function testWillCreateFactoryWithoutProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + + $factory = new Factory($container); + $this->assertInstanceOf(Factory::class, $factory); + } + + /** + * @throws Exception + */ + public function testWillCreateFactoryWithProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + $manager = $this->createMock(ProviderPluginManager::class); + + $factory = new Factory($container, $manager); + $this->assertInstanceOf(Factory::class, $factory); + } + + /** + * @throws Exception + */ + public function testFactoryWillNotCreateProviderWithoutProviderType(): void + { + $container = $this->createMock(ContainerInterface::class); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Undefined navigation provider type'); + $factory = new Factory($container); + $factory->create([]); + } + + /** + * @throws Exception + */ + public function testFactoryWillNotCreateProviderWithInvalidProviderType(): void + { + $container = $this->createMock(ContainerInterface::class); + + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage( + sprintf( + 'A plugin by the name "test" was not found in the plugin manager %s', + ProviderPluginManager::class + ) + ); + $factory = new Factory($container); + $factory->create([ + 'type' => 'test', + ]); + } + + /** + * @throws Exception + */ + public function testFactoryWillCreateProviderWithValidProviderTypeAndNoOptions(): void + { + $container = $this->createMock(ContainerInterface::class); + + $factory = new Factory($container); + $provider = $factory->create([ + 'type' => ArrayProvider::class, + ]); + $this->assertInstanceOf(ProviderInterface::class, $provider); + } + + /** + * @throws Exception + */ + public function testFactoryWillCreateProviderWithValidProviderTypeAndOptions(): void + { + $container = $this->createMock(ContainerInterface::class); + + $factory = new Factory($container); + $provider = $factory->create([ + 'type' => ArrayProvider::class, + 'options' => [], + ]); + $this->assertInstanceOf(ProviderInterface::class, $provider); + } + + /** + * @throws Exception + */ + public function testFactoryWillGetProviderPluginManagerWithoutInitialProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + + $factory = new Factory($container); + $this->assertInstanceOf(ProviderPluginManager::class, $factory->getProviderPluginManager()); + } + + /** + * @throws Exception + */ + public function testFactoryWillGetProviderPluginManagerWithInitialProviderPluginManager(): void + { + $container = $this->createMock(ContainerInterface::class); + $manager = $this->createMock(ProviderPluginManager::class); + + $factory = new Factory($container, $manager); + $this->assertInstanceOf(ProviderPluginManager::class, $factory->getProviderPluginManager()); + } +} diff --git a/test/Service/NavigationTest.php b/test/Service/NavigationTest.php new file mode 100644 index 0000000..01c6574 --- /dev/null +++ b/test/Service/NavigationTest.php @@ -0,0 +1,335 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\Service; + +use Dot\Authorization\AuthorizationInterface; +use Dot\Helpers\Route\RouteHelper; +use Dot\Navigation\Exception\RuntimeException; +use Dot\Navigation\NavigationContainer; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Page; +use Dot\Navigation\Provider\ArrayProvider; +use Dot\Navigation\Provider\FactoryInterface; +use Dot\Navigation\Service\Navigation; +use Dot\Navigation\Service\NavigationInterface; +use Mezzio\Router\RouteResult; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; + +class NavigationTest extends TestCase +{ + /** + * @throws Exception + */ + public function testNavigationWillInitialize(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertInstanceOf(NavigationInterface::class, $navigation); + } + + /** + * @throws Exception + */ + public function testAccessors(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $routeResult = $this->createMock(RouteResult::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertNull($navigation->getRouteResult()); + $navigation->setRouteResult($routeResult); + $this->assertInstanceOf(RouteResult::class, $navigation->getRouteResult()); + $this->assertTrue($navigation->getIsActiveRecursion()); + $navigation->setIsActiveRecursion(false); + $this->assertFalse($navigation->getIsActiveRecursion()); + } + + /** + * @throws Exception + */ + public function testNavigationWillNotGetInvalidContainer(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Container `test` is not defined'); + $navigation = new Navigation($factory, $route, $options); + $navigation->getContainer('test'); + } + + /** + * @throws Exception + */ + public function testNavigationWillGetValidContainer(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + + $options = new NavigationOptions([ + 'containers' => [ + 'default' => [ + 'type' => ArrayProvider::class, + ], + ], + ]); + + $navigation = new Navigation($factory, $route, $options); + $this->assertInstanceOf(NavigationContainer::class, $navigation->getContainer('default')); + } + + /** + * @throws Exception + */ + public function testIsAllowedWillReturnTrueWithoutAuthorization(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertTrue($navigation->isAllowed(new Page())); + } + + /** + * @throws Exception + */ + public function testIsAllowedWillReturnTrueWithAuthorizationWhenPageHasNoPermission(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $authorization = $this->createMock(AuthorizationInterface::class); + + $navigation = new Navigation($factory, $route, $options, $authorization); + $this->assertTrue($navigation->isAllowed(new Page())); + } + + /** + * @throws Exception + */ + public function testIsAllowedWillReturnTrueWithAuthorizationWhenPageHasNoRoles(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $authorization = $this->createMock(AuthorizationInterface::class); + + $authorization->expects($this->once())->method('isGranted')->willReturn(true); + + $page = new Page(); + $page->setOption('permission', ''); + $navigation = new Navigation($factory, $route, $options, $authorization); + $this->assertTrue($navigation->isAllowed($page)); + } + + /** + * @throws Exception + */ + public function testIsAllowedWillReturnTrueWithAuthorizationWhenPageHasPermissionsAndRoles(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $authorization = $this->createMock(AuthorizationInterface::class); + + $authorization->expects($this->once())->method('isGranted')->willReturn(true); + + $page = new Page(); + $page->setOption('permission', ''); + $page->setOption('roles', []); + $navigation = new Navigation($factory, $route, $options, $authorization); + $this->assertTrue($navigation->isAllowed($page)); + } + + /** + * @throws Exception + */ + public function testIsActiveWillCacheResults(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertIsArray($navigation->getIsActiveCache()); + $this->assertEmpty($navigation->getIsActiveCache()); + $navigation->isActive(new Page()); + $this->assertIsArray($navigation->getIsActiveCache()); + $this->assertCount(1, $navigation->getIsActiveCache()); + } + + /** + * @throws Exception + */ + public function testIsActiveWillReturnFalseWithoutRouteResult(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertFalse($navigation->isActive(new Page())); + } + + /** + * @throws Exception + */ + public function testIsActiveWillReturnFalseWithoutSuccessfulRouteResult(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $routeResult = $this->createMock(RouteResult::class); + + $routeResult->expects($this->once())->method('isSuccess')->willReturn(false); + + $navigation = new Navigation($factory, $route, $options); + $navigation->setRouteResult($routeResult); + $this->assertFalse($navigation->isActive(new Page())); + } + + /** + * @throws Exception + */ + public function testIsActiveWillReturnFalseWhenPageHasNoRoute(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $routeResult = $this->createMock(RouteResult::class); + + $routeResult->expects($this->once())->method('isSuccess')->willReturn(true); + + $navigation = new Navigation($factory, $route, $options); + $navigation->setRouteResult($routeResult); + $this->assertFalse($navigation->isActive(new Page())); + } + + /** + * @throws Exception + */ + public function testIsActiveWillReturnTrueWhenRequestedRouteMatchesPageRoute(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $routeResult = $this->createMock(RouteResult::class); + + $routeResult->expects($this->once())->method('isSuccess')->willReturn(true); + $routeResult->expects($this->once())->method('getMatchedRouteName')->willReturn('test'); + + $page = new Page(); + $page->setOption('route', [ + 'route_name' => 'test', + ]); + $navigation = new Navigation($factory, $route, $options); + $navigation->setRouteResult($routeResult); + $navigation->setIsActiveRecursion(false); + $this->assertTrue($navigation->isActive($page)); + } + + /** + * @throws Exception + */ + public function testIsActiveWillReturnTrueWhenRequestedRouteMatchesChildPageRoute(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + $routeResult = $this->createMock(RouteResult::class); + + $routeResult->expects($this->any())->method('isSuccess')->willReturn(true); + $routeResult->expects($this->any())->method('getMatchedRouteName')->willReturn('child'); + + $childPage = new Page(); + $childPage->setOption('route', [ + 'route_name' => 'child', + ]); + $parentPage = new Page(); + $parentPage->setOption('route', [ + 'route_name' => 'parent', + ]); + $parentPage->addPage($childPage); + $navigation = new Navigation($factory, $route, $options); + $navigation->setRouteResult($routeResult); + $navigation->setIsActiveRecursion(true); + $this->assertTrue($navigation->isActive($parentPage)); + } + + /** + * @throws Exception + */ + public function testGetHrefWillCacheResults(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + $this->assertIsArray($navigation->getHrefCache()); + $this->assertEmpty($navigation->getHrefCache()); + + $page = new Page(); + $page->setOption('uri', 'page1'); + $navigation->getHref($page); + $this->assertIsArray($navigation->getHrefCache()); + $this->assertCount(1, $navigation->getHrefCache()); + + $page = new Page(); + $page->setOption('route', [ + 'route_name' => 'page2', + ]); + $navigation->getHref($page); + $this->assertIsArray($navigation->getHrefCache()); + $this->assertCount(2, $navigation->getHrefCache()); + } + + /** + * @throws Exception + */ + public function testWillNotGetHrefForInvalidPage(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/^Unable to assemble href for navigation page.*/'); + $page = new Page(); + $navigation->getHref($page); + } + + /** + * @throws Exception + */ + public function testWillGetHrefForValidPage(): void + { + $factory = $this->createMock(FactoryInterface::class); + $route = $this->createMock(RouteHelper::class); + $options = $this->createMock(NavigationOptions::class); + + $navigation = new Navigation($factory, $route, $options); + + $page = new Page(); + $page->setOption('uri', 'page1'); + $this->assertSame('page1', $navigation->getHref($page)); + + $page = new Page(); + $page->setOption('route', [ + 'route_name' => 'page2', + ]); + $this->assertIsString($navigation->getHref($page)); + } +} diff --git a/test/View/NavigationRendererTest.php b/test/View/NavigationRendererTest.php new file mode 100644 index 0000000..12d25da --- /dev/null +++ b/test/View/NavigationRendererTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace DotTest\Navigation\View; + +use Dot\Navigation\NavigationContainer; +use Dot\Navigation\Options\NavigationOptions; +use Dot\Navigation\Service\NavigationInterface; +use Dot\Navigation\View\NavigationRenderer; +use Mezzio\Template\TemplateRendererInterface; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; + +class NavigationRendererTest extends TestCase +{ + /** + * @throws Exception + */ + public function testWillCreateNavigationRenderer(): void + { + $navigation = $this->createMock(NavigationInterface::class); + $template = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + + $renderer = new NavigationRenderer($navigation, $template, $options); + $this->assertInstanceOf(NavigationRenderer::class, $renderer); + } + + /** + * @throws Exception + */ + public function testWillRenderPartial(): void + { + $navigation = $this->createMock(NavigationInterface::class); + $template = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + + $renderer = new NavigationRenderer($navigation, $template, $options); + $html = $renderer->renderPartial(new NavigationContainer(), 'partial'); + $this->assertIsString($html); + } + + /** + * @throws Exception + */ + public function testWillRenderTemplate(): void + { + $navigation = $this->createMock(NavigationInterface::class); + $template = $this->createMock(TemplateRendererInterface::class); + $options = $this->createMock(NavigationOptions::class); + + $renderer = new NavigationRenderer($navigation, $template, $options); + $html = $renderer->render(new NavigationContainer(), 'template'); + $this->assertIsString($html); + } +}