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
+
+
+
+
+
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);
+ }
+}