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 ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-navigation) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-navigation/3.2.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-navigation/3.4.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-navigation)](https://github.com/dotkernel/dot-navigation/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-navigation)](https://github.com/dotkernel/dot-navigation/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-navigation)](https://github.com/dotkernel/dot-navigation/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-navigation)](https://github.com/dotkernel/dot-navigation/blob/3.2.0/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-navigation)](https://github.com/dotkernel/dot-navigation/blob/3.0/LICENSE.md) + +[![SymfonyInsight](https://insight.symfony.com/projects/68b7c728-4cc9-40ac-a3be-cf17f9b2eaf1/big.svg)](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 @@ + + + + + + + + + + + + + + src + test + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6de330b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./test + + + + + + ./src + + + 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 @@ + + + + + IsAllowedFilter + + + + + RecursiveIterator + + + + + ProviderPluginManager + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9dd8f07 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,18 @@ + + + + + + + + + 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 @@ $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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ __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 @@ 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 @@ 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 @@ 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 @@ + 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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); + } +}