Skip to content

Commit

Permalink
Issue #12: Code quality
Browse files Browse the repository at this point in the history
Signed-off-by: alexmerlin <[email protected]>
alexmerlin committed Jul 18, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 5247bf3 commit 63b4506
Showing 52 changed files with 2,269 additions and 872 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/cs-tests.yml
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
67 changes: 9 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

35 changes: 26 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
@@ -17,20 +17,26 @@
"email": "[email protected]"
}
],
"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"
}
}
File renamed without changes.
20 changes: 20 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">

<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>

<!-- Show progress -->
<arg value="p"/>

<!-- Paths to check -->
<file>src</file>
<file>test</file>

<!-- Include all rules from the Laminas Coding Standard -->
<rule ref="LaminasCodingStandard"/>
</ruleset>
17 changes: 17 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
bootstrap="./vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Dot-Navigation Test Suite">
<directory>./test</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
18 changes: 18 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.13.1@086b94371304750d1c673315321a55d15fc59015">
<file src="src/Filter/IsAllowedFilter.php">
<MissingTemplateParam>
<code>IsAllowedFilter</code>
</MissingTemplateParam>
</file>
<file src="src/NavigationContainer.php">
<MissingTemplateParam>
<code>RecursiveIterator</code>
</MissingTemplateParam>
</file>
<file src="src/Provider/ProviderPluginManager.php">
<MissingTemplateParam>
<code>ProviderPluginManager</code>
</MissingTemplateParam>
</file>
</files>
18 changes: 18 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
resolveFromConfigFile="true"
findUnusedCode="false"
findUnusedBaselineEntry="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>
44 changes: 14 additions & 30 deletions src/ConfigProvider.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation;

@@ -15,54 +10,43 @@
use Dot\Navigation\Factory\NavigationServiceFactory;
use Dot\Navigation\Factory\ProviderPluginManagerFactory;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Provider\Factory;
use Dot\Navigation\Provider\FactoryInterface;
use Dot\Navigation\Provider\ProviderPluginManager;
use Dot\Navigation\Service\Navigation;
use Dot\Navigation\Service\NavigationInterface;
use Dot\Navigation\View\NavigationRenderer;
use Dot\Navigation\View\RendererInterface;

/**
* Class ConfigProvider
* @package Dot\Navigation
*/
class ConfigProvider
{
/**
* @return array
*/
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencyConfig(),

'dependencies' => $this->getDependencyConfig(),
'dot_navigation' => [

'active_recursion' => true,

'containers' => [],

'provider_manager' => []
'containers' => [],
'provider_manager' => [],
],
];
}

/**
* @return array
*/
public function getDependencyConfig(): array
{
return [
'factories' => [
NavigationOptions::class => NavigationOptionsFactory::class,
Navigation::class => NavigationServiceFactory::class,
NavigationMiddleware::class => NavigationMiddlewareFactory::class,
NavigationOptions::class => NavigationOptionsFactory::class,
NavigationRenderer::class => NavigationRendererFactory::class,
ProviderPluginManager::class => ProviderPluginManagerFactory::class,
Navigation::class => NavigationServiceFactory::class,
NavigationRenderer::class => NavigationRendererFactory::class,
NavigationMiddleware::class => NavigationMiddlewareFactory::class,
],
'aliases' => [
'aliases' => [
FactoryInterface::class => Factory::class,
NavigationInterface::class => Navigation::class,
RendererInterface::class => NavigationRenderer::class,
]
RendererInterface::class => NavigationRenderer::class,
],
];
}
}
12 changes: 1 addition & 11 deletions src/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Exception;

/**
* Interface ExceptionInterface
* @package Dot\Navigation\Exception
*/
interface ExceptionInterface
{

}
12 changes: 1 addition & 11 deletions src/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Exception;

/**
* Class InvalidArgumentException
* @package Dot\Navigation\Exception
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{

}
12 changes: 1 addition & 11 deletions src/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Exception;

/**
* Class RuntimeException
* @package Dot\Navigation\Exception
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{

}
33 changes: 17 additions & 16 deletions src/Factory/NavigationMiddlewareFactory.php
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Factory;

use Dot\Navigation\NavigationMiddleware;
use Dot\Navigation\Service\NavigationInterface;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
* Class NavigationMiddlewareFactory
* @package Dot\Navigation\Factory
*/
class NavigationMiddlewareFactory
{
public const MESSAGE_MISSING_NAVIGATION = 'Unable to find NavigationInterface in the container';

/**
* @param ContainerInterface $container
* @param $requestedName
* @return NavigationMiddleware
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function __invoke(ContainerInterface $container, $requestedName): NavigationMiddleware
public function __invoke(ContainerInterface $container): NavigationMiddleware
{
$navigation = $container->get(NavigationInterface::class);
return new $requestedName($navigation);
if (! $container->has(NavigationInterface::class)) {
throw new Exception(self::MESSAGE_MISSING_NAVIGATION);
}

return new NavigationMiddleware(
$container->get(NavigationInterface::class)
);
}
}
46 changes: 30 additions & 16 deletions src/Factory/NavigationOptionsFactory.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Factory;

use Dot\Navigation\Options\NavigationOptions;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

use function array_key_exists;
use function is_array;

/**
* Class NavigationOptionsFactory
* @package Dot\Navigation\Factory
*/
class NavigationOptionsFactory
{
public const MESSAGE_MISSING_CONFIG = 'Unable to find config in the container';
public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-navigation config';

/**
* @param ContainerInterface $container
* @param $requestedName
* @return NavigationOptions
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function __invoke(ContainerInterface $container, $requestedName): NavigationOptions
public function __invoke(ContainerInterface $container): NavigationOptions
{
$config = $container->get('config')['dot_navigation'];
return new $requestedName($config);
if (! $container->has('config')) {
throw new Exception(self::MESSAGE_MISSING_CONFIG);
}
$config = $container->get('config');

if (
! array_key_exists('dot_navigation', $config)
|| ! is_array($config['dot_navigation'])
|| empty($config['dot_navigation'])
) {
throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG);
}

return new NavigationOptions(
$config['dot_navigation']
);
}
}
48 changes: 29 additions & 19 deletions src/Factory/NavigationRendererFactory.php
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Factory;

use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Service\NavigationInterface;
use Dot\Navigation\View\NavigationRenderer;
use Psr\Container\ContainerInterface;
use Exception;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
* Class NavigationRendererFactory
* @package Dot\Navigation\Factory
*/
class NavigationRendererFactory
{
public const MESSAGE_MISSING_NAVIGATION_INTERFACE = 'Unable to find NavigationInterface in the container';
public const MESSAGE_MISSING_NAVIGATION_OPTIONS = 'Unable to find NavigationOptions in the container';
public const MESSAGE_MISSING_TEMPLATE_RENDERER = 'Unable to find TemplateRendererInterface in the container';

/**
* @param ContainerInterface $container
* @param $requestedName
* @return NavigationRenderer
* @throws NotFoundExceptionInterface
* @throws ContainerExceptionInterface
* @throws Exception
*/
public function __invoke(ContainerInterface $container, $requestedName): NavigationRenderer
public function __invoke(ContainerInterface $container): NavigationRenderer
{
$options = $container->get(NavigationOptions::class);
$navigation = $container->get(NavigationInterface::class);
$template = $container->get(TemplateRendererInterface::class);
if (! $container->has(NavigationInterface::class)) {
throw new Exception(self::MESSAGE_MISSING_NAVIGATION_INTERFACE);
}

if (! $container->has(TemplateRendererInterface::class)) {
throw new Exception(self::MESSAGE_MISSING_TEMPLATE_RENDERER);
}

if (! $container->has(NavigationOptions::class)) {
throw new Exception(self::MESSAGE_MISSING_NAVIGATION_OPTIONS);
}

return new $requestedName($navigation, $template, $options);
return new NavigationRenderer(
$container->get(NavigationInterface::class),
$container->get(TemplateRendererInterface::class),
$container->get(NavigationOptions::class)
);
}
}
51 changes: 30 additions & 21 deletions src/Factory/NavigationServiceFactory.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Factory;

@@ -15,33 +10,47 @@
use Dot\Navigation\Provider\Factory;
use Dot\Navigation\Provider\ProviderPluginManager;
use Dot\Navigation\Service\Navigation;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
* Class NavigationServiceFactory
* @package Dot\Navigation\Factory
*/
class NavigationServiceFactory
{
public const MESSAGE_MISSING_PLUGIN_MANAGER = 'Unable to find RouteHelper in the container';
public const MESSAGE_MISSING_ROUTE_HELPER = 'Unable to find ProviderPluginManager in the container';
public const MESSAGE_MISSING_NAVIGATION_OPTIONS = 'Unable to find NavigationOptions in the container';

/**
* @param ContainerInterface $container
* @param $requestedName
* @return Navigation
* @throws NotFoundExceptionInterface
* @throws ContainerExceptionInterface
* @throws Exception
*/
public function __invoke(ContainerInterface $container, $requestedName): Navigation
public function __invoke(ContainerInterface $container): Navigation
{
$routeHelper = $container->get(RouteHelper::class);
$authorization = $container->has(AuthorizationInterface::class)
? $container->get(AuthorizationInterface::class)
: null;
if (! $container->has(RouteHelper::class)) {
throw new Exception(self::MESSAGE_MISSING_ROUTE_HELPER);
}

if (! $container->has(ProviderPluginManager::class)) {
throw new Exception(self::MESSAGE_MISSING_PLUGIN_MANAGER);
}

$providerFactory = new Factory($container, $container->get(ProviderPluginManager::class));
if (! $container->has(NavigationOptions::class)) {
throw new Exception(self::MESSAGE_MISSING_NAVIGATION_OPTIONS);
}

/** @var NavigationOptions $options */
$options = $container->get(NavigationOptions::class);

/** @var Navigation $service */
$service = new $requestedName($providerFactory, $routeHelper, $options, $authorization);
$service = new Navigation(
new Factory($container, $container->get(ProviderPluginManager::class)),
$container->get(RouteHelper::class),
$options,
$container->has(AuthorizationInterface::class)
? $container->get(AuthorizationInterface::class)
: null
);
$service->setIsActiveRecursion($options->getActiveRecursion());

return $service;
51 changes: 37 additions & 14 deletions src/Factory/ProviderPluginManagerFactory.php
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Factory;

use Dot\Navigation\Provider\ProviderPluginManager;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

use function array_key_exists;
use function is_array;

/**
* Class ProviderPluginManagerFactory
* @package Dot\Navigation\Factory
*/
class ProviderPluginManagerFactory
{
public const MESSAGE_MISSING_CONFIG = 'Unable to find config in the container';
public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-navigation config';
public const MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER = 'Missing/invalid dot-navigation config: provider_manager';

/**
* @param ContainerInterface $container
* @return ProviderPluginManager
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function __invoke(ContainerInterface $container)
public function __invoke(ContainerInterface $container): ProviderPluginManager
{
$config = $container->get('config')['dot_navigation']['provider_manager'];
if (! $container->has('config')) {
throw new Exception(self::MESSAGE_MISSING_CONFIG);
}
$config = $container->get('config');

if (
! array_key_exists('dot_navigation', $config)
|| ! is_array($config['dot_navigation'])
|| empty($config['dot_navigation'])
) {
throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG);
}
$config = $config['dot_navigation'];

if (
! array_key_exists('provider_manager', $config)
|| ! is_array($config['provider_manager'])
) {
throw new Exception(self::MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER);
}
$config = $config['provider_manager'];

return new ProviderPluginManager($container, $config);
}
}
33 changes: 7 additions & 26 deletions src/Filter/IsAllowedFilter.php
Original file line number Diff line number Diff line change
@@ -1,50 +1,31 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Filter;

use Dot\Navigation\Service\NavigationInterface;
use RecursiveFilterIterator;
use RecursiveIterator;

/**
* Class IsAllowedFilter
* @package Dot\Navigation\Filter
*/
class IsAllowedFilter extends \RecursiveFilterIterator
class IsAllowedFilter extends RecursiveFilterIterator
{
/** @var NavigationInterface */
protected $navigation;
protected NavigationInterface $navigation;

/**
* IsAllowedFilter constructor.
* @param \RecursiveIterator $iterator
* @param NavigationInterface $navigation
*/
public function __construct(\RecursiveIterator $iterator, NavigationInterface $navigation)
public function __construct(RecursiveIterator $iterator, NavigationInterface $navigation)
{
$this->navigation = $navigation;
parent::__construct($iterator);
}

/**
* @return bool
*/
public function accept(): bool
{
return $this->navigation->isAllowed($this->current());
}

/**
* @return IsAllowedFilter
*/
public function getChildren(): IsAllowedFilter
{
/** @var \RecursiveIterator $innerIterator */
/** @var RecursiveIterator $innerIterator */
$innerIterator = $this->getInnerIterator();
return new self($innerIterator->getChildren(), $this->navigation);
}
130 changes: 34 additions & 96 deletions src/NavigationContainer.php
Original file line number Diff line number Diff line change
@@ -1,191 +1,129 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation;

/**
* Class Container
* @package Dot\Navigation
*/
class NavigationContainer implements \RecursiveIterator
{
/**
* Index of current active child
* @var int
*/
protected $index = 0;
use RecursiveIterator;
use RecursiveIteratorIterator;

/**
* Child nodes
* @var array
*/
protected $children = [];
use function count;

class NavigationContainer implements RecursiveIterator
{
protected int $index = 0;
protected array $children = [];

/**
* NavigationContainer constructor.
* @param array $pages
*/
public function __construct(array $pages = [])
{
$this->addPages($pages);
}

/**
* @param array $pages
* @param Page[] $pages
*/
public function addPages(array $pages)
public function addPages(array $pages): void
{
foreach ($pages as $page) {
$this->addPage($page);
}
}

/**
* @param Page $page
*/
public function addPage(Page $page)
public function addPage(Page $page): void
{
$this->children[] = $page;
}

/**
* @return NavigationContainer
*/
public function current(): NavigationContainer
{
return $this->children[$this->index];
}

/**
* Increment current position to the next element
*/
public function next()
public function next(): void
{
$this->index++;
}

/**
* @return int
*/
public function key(): int
{
return $this->index;
}

/**
* @return bool
*/
public function valid(): bool
{
return isset($this->children[$this->index]);
}

/**
* Reset position to the first element
*/
public function rewind()
public function rewind(): void
{
$this->index = 0;
}

/**
* @return bool
*/
public function hasChildren(): bool
{
return count($this->children) > 0;
}

/**
* @return NavigationContainer
*/
public function getChildren(): NavigationContainer
{
return $this->children[$this->index];
}

/**
* Find a single child by attribute
*
* @param string $attribute
* @param mixed $value
* @return Page|null
*/
public function findOneByAttribute(string $attribute, $value): ?Page
public function findOneByAttribute(string $attribute, mixed $value): ?Page
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);

/** @var Page $page */
foreach ($iterator as $page) {
if ($page->getAttribute($attribute) === $value) {
return $page;
}
}

return null;
}

/**
* Find all children by attribute
*
* @param string $attribute
* @param mixed $value
* @return array
*/
public function findByAttribute(string $attribute, $value): array
public function findByAttribute(string $attribute, mixed $value): array
{
$result = [];
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
$result = [];
$iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);

/** @var Page $page */
foreach ($iterator as $page) {
if ($page->getAttribute($attribute) == $value) {
if ($page->getAttribute($attribute) === $value) {
$result[] = $page;
}
}

return $result;
}

/**
* Finds a single child by option.
*
* @param string $option
* @param mixed $value
* @return Page|null
*/
public function findOneByOption(string $option, $value): ?Page
public function findOneByOption(string $option, mixed $value): ?Page
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);

/** @var Page $page */
foreach ($iterator as $page) {
if ($page->getOption($option) == $value) {
if ($page->getOption($option) === $value) {
return $page;
}
}

return null;
}

/**
* Finds all children by option.
*
* @param string $option
* @param mixed $value
* @return array
*/
public function findByOption(string $option, $value): array
public function findByOption(string $option, mixed $value): array
{
$result = [];
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
$result = [];
$iterator = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);

/** @var Page $page */
foreach ($iterator as $page) {
if ($page->getOption($option) == $value) {
if ($page->getOption($option) === $value) {
$result[] = $page;
}
}

return $result;
}
}
31 changes: 5 additions & 26 deletions src/NavigationMiddleware.php
Original file line number Diff line number Diff line change
@@ -1,50 +1,29 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation;

use Dot\Navigation\Service\NavigationInterface;
use Mezzio\Router\RouteResult;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Mezzio\Router\RouteResult;

/**
* Class NavigationMiddleware
* @package Dot\Navigation
*/
class NavigationMiddleware implements MiddlewareInterface
{
/**
* @var NavigationInterface
*/
protected $navigation;
protected NavigationInterface $navigation;

/**
* NavigationMiddleware constructor.
* @param NavigationInterface $navigation
*/
public function __construct(NavigationInterface $navigation)
{
$this->navigation = $navigation;
}

/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$routeResult = $request->getAttribute(RouteResult::class, null);
if ($routeResult) {
$routeResult = $request->getAttribute(RouteResult::class);
if ($routeResult instanceof RouteResult) {
$this->navigation->setRouteResult($routeResult);
}

44 changes: 8 additions & 36 deletions src/Options/NavigationOptions.php
Original file line number Diff line number Diff line change
@@ -1,66 +1,38 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Options;

use Laminas\Stdlib\AbstractOptions;

/**
* Class NavigationOptions
* @package Dot\Navigation\Options
*/
class NavigationOptions extends AbstractOptions
{
/** @var array */
protected $containers;
protected array $containers = [];
protected bool $activeRecursion = true;

/** @var bool */
protected $activeRecursion = true;

/**
* NavigationOptions constructor.
* @param array $options
*/
public function __construct($options = null)
public function __construct(?array $options = null)
{
$this->__strictMode__ = false;
parent::__construct($options);
}

/**
* @return mixed
*/
public function getContainers()
public function getContainers(): array
{
return $this->containers;
}

/**
* @param mixed $containers
*/
public function setContainers($containers)
public function setContainers(array $containers): void
{
$this->containers = $containers;
}

/**
* @return boolean
*/
public function getActiveRecursion()
public function getActiveRecursion(): bool
{
return $this->activeRecursion;
}

/**
* @param boolean $activeRecursion
*/
public function setActiveRecursion($activeRecursion)
public function setActiveRecursion(bool $activeRecursion): void
{
$this->activeRecursion = $activeRecursion;
}
113 changes: 29 additions & 84 deletions src/Page.php
Original file line number Diff line number Diff line change
@@ -1,153 +1,98 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation;

/**
* Class Page
* @package Dot\Navigation
*/
use function count;
use function is_string;

class Page extends NavigationContainer
{
/**
* @var array
*/
protected $attributes = [];

/**
* @var array
*/
protected $options = [];

/**
* @var null|Page
*/
protected $parent;

/**
* @return bool
*/
protected ?Page $parent = null;
protected array $attributes = [];
protected array $options = [];

public function hasParent(): bool
{
return null !== $this->parent;
return $this->parent instanceof Page;
}

/**
* @return Page|null
*/
public function getParent(): ?Page
{
return $this->parent;
}

/**
* @param NavigationContainer $parent
*/
public function setParent(NavigationContainer $parent)
public function setParent(Page $parent): void
{
$this->parent = $parent;
}

/**
* @param Page $page
*/
public function addPage(Page $page)
public function addPage(Page $page): void
{
$page->setParent($this);
parent::addPage($page);
}

/**
* @param string $option
* @param mixed $value
*/
public function setOption(string $option, $value)
public function setOption(string $option, mixed $value): void
{
$this->options[$option] = $value;
}

/**
* @return array
*/
public function getOptions(): array
{
return $this->options;
}

/**
* @param array $options
*/
public function setOptions(array $options)
public function hasOptions(): bool
{
return count($this->options) > 0;
}

public function setOptions(array $options): void
{
$this->options = $options;
}

/**
* @param string $attribute
* @param mixed $value
*/
public function setAttribute(string $attribute, $value)
public function setAttribute(string $attribute, mixed $value): void
{
$this->attributes[$attribute] = $value;
}

/**
* @param string $attribute
* @return mixed
*/
public function getAttribute(string $attribute)
public function getAttribute(string $attribute): mixed
{
return $this->attributes[$attribute] ?? null;
}

/**
* @return array
*/
public function getAttributes(): array
{
return $this->attributes;
}

/**
* @param array $attributes
*/
public function setAttributes(array $attributes)
public function hasAttributes(): bool
{
return count($this->attributes) > 0;
}

public function setAttributes(array $attributes): void
{
$this->attributes = $attributes;
}

/**
* @return string
*/
public function getName(): ?string
{
return $this->getOption('name');
}

/**
* @param string $option
* @return mixed
*/
public function getOption(string $option)
public function getOption(string $option): mixed
{
return $this->options[$option] ?? null;
}

/**
* @return string
*/
public function getLabel(): string
{
$label = $this->getOption('label');
if (!is_string($label) || empty($label)) {
$label = 'Not defined';
}
return $label;

return ! is_string($label) || empty($label) ? 'Not defined' : $label;
}
}
54 changes: 14 additions & 40 deletions src/Provider/ArrayProvider.php
Original file line number Diff line number Diff line change
@@ -1,48 +1,27 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Provider;

use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Page;

/**
* Class ArrayProvider
* @package Dot\Navigation\Provider
*/
use function count;
use function is_array;

class ArrayProvider implements ProviderInterface
{
/**
* @var NavigationContainer
*/
protected $container;

/**
* @var array
*/
protected $items = [];

/**
* ArrayProvider constructor.
* @param array $options
*/
public function __construct(array $options = null)
protected ?NavigationContainer $container = null;
protected array $items = [];

public function __construct(?array $options = [])
{
$options = $options ?? [];
if (isset($options['items']) && is_array($options['items'])) {
$this->setItems($options['items']);
}
}

/**
* @return NavigationContainer
*/
public function getContainer(): NavigationContainer
{
if ($this->container instanceof NavigationContainer) {
@@ -58,10 +37,6 @@ public function getContainer(): NavigationContainer
return $this->container;
}

/**
* @param array $spec
* @return Page
*/
protected function getPage(array $spec): Page
{
$page = new Page();
@@ -83,18 +58,17 @@ protected function getPage(array $spec): Page
return $page;
}

/**
* @return array
*/
public function getItems(): array
{
return $this->items;
}

/**
* @param array $items
*/
public function setItems(array $items)
public function hasItems(): bool
{
return count($this->items) > 0;
}

public function setItems(array $items): void
{
$this->items = $items;
}
41 changes: 8 additions & 33 deletions src/Provider/Factory.php
Original file line number Diff line number Diff line change
@@ -1,61 +1,36 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Provider;

use Dot\Navigation\Exception\RuntimeException;
use Psr\Container\ContainerInterface;

/**
* Class Factory
* @package Dot\Navigation\Provider
*/
class Factory
class Factory implements FactoryInterface
{
/** @var ContainerInterface */
protected $container;
protected ContainerInterface $container;
protected ?ProviderPluginManager $providerPluginManager;

/** @var ProviderPluginManager */
protected $providerPluginManager;

/**
* Factory constructor.
* @param ContainerInterface $container
* @param ProviderPluginManager|null $providerPluginManager
*/
public function __construct(ContainerInterface $container, ProviderPluginManager $providerPluginManager = null)
public function __construct(ContainerInterface $container, ?ProviderPluginManager $providerPluginManager = null)
{
$this->container = $container;
$this->container = $container;
$this->providerPluginManager = $providerPluginManager;
}

/**
* @param array $specs
* @return ProviderInterface
*/
public function create(array $specs): ProviderInterface
{
$type = $specs['type'] ?? '';
if (empty($type)) {
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, []);
}

12 changes: 12 additions & 0 deletions src/Provider/FactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Dot\Navigation\Provider;

interface FactoryInterface
{
public function create(array $specs): ProviderInterface;

public function getProviderPluginManager(): ProviderPluginManager;
}
14 changes: 1 addition & 13 deletions src/Provider/ProviderInterface.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Provider;

use Dot\Navigation\NavigationContainer;

/**
* Interface ProviderInterface
* @package Dot\Navigation\Provider
*/
interface ProviderInterface
{
/**
* @return NavigationContainer
*/
public function getContainer(): NavigationContainer;
}
18 changes: 5 additions & 13 deletions src/Provider/ProviderPluginManager.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Provider;

use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Factory\InvokableFactory;

/**
* Class ProviderPluginManager
* @package Dot\Navigation\Provider
*/
class ProviderPluginManager extends AbstractPluginManager
{
/** @var string */
/** @var string $instanceOf */
protected $instanceOf = ProviderInterface::class;

/** @var array */
protected $factories = [
ArrayProvider::class => InvokableFactory::class,
];

/** @var string[] $aliases */
protected $aliases = [
'arrayprovider' => ArrayProvider::class,
'arrayProvider' => ArrayProvider::class,
'ArrayProvider' => ArrayProvider::class,
'array' => ArrayProvider::class,
'Array' => ArrayProvider::class,
'array' => ArrayProvider::class,
'Array' => ArrayProvider::class,
];
}
182 changes: 49 additions & 133 deletions src/Service/Navigation.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Service;

@@ -15,155 +10,94 @@
use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Page;
use Dot\Navigation\Provider\Factory;
use Dot\Navigation\Provider\ProviderInterface;
use Dot\Navigation\Provider\FactoryInterface;
use Mezzio\Router\RouteResult;
use RecursiveIteratorIterator;

use function array_diff;
use function array_merge;
use function spl_object_hash;
use function sprintf;

/**
* Class Navigation
* @package Dot\Navigation\Service
*/
class Navigation implements NavigationInterface
{
/**
* @var NavigationContainer[]
*/
protected $containers = [];

/**
* @var Factory
*/
protected $providerFactory;

/** @var RouteHelper */
protected $routeHelper;

/**
* @var NavigationOptions
*/
protected $moduleOptions;

/**
* @var AuthorizationInterface
*/
protected $authorization;

/**
* @var RouteResult
*/
protected $routeResult;

/**
* @var array
*/
protected $isActiveCache = [];

/**
* @var array
*/
protected $hrefCache = [];

/**
* @var bool
*/
protected $isActiveRecursion = true;
protected FactoryInterface $providerFactory;
protected RouteHelper $routeHelper;
protected NavigationOptions $moduleOptions;
protected ?AuthorizationInterface $authorization;
protected ?RouteResult $routeResult = null;
protected array $containers = [];
protected array $isActiveCache = [];
protected array $hrefCache = [];
protected bool $isActiveRecursion = true;

/**
* NavigationService constructor.
* @param Factory $providerFactory
* @param AuthorizationInterface $authorization
* @param RouteHelper $routeHelper
* @param NavigationOptions $moduleOptions
*/
public function __construct(
Factory $providerFactory,
FactoryInterface $providerFactory,
RouteHelper $routeHelper,
NavigationOptions $moduleOptions,
AuthorizationInterface $authorization = null
?AuthorizationInterface $authorization = null
) {
$this->routeHelper = $routeHelper;
$this->authorization = $authorization;
$this->routeHelper = $routeHelper;
$this->authorization = $authorization;
$this->providerFactory = $providerFactory;
$this->moduleOptions = $moduleOptions;
$this->moduleOptions = $moduleOptions;
}

/**
* @return RouteResult
*/
public function getRouteResult(): RouteResult
public function getRouteResult(): ?RouteResult
{
return $this->routeResult;
}

/**
* @param RouteResult $routeResult
*/
public function setRouteResult(RouteResult $routeResult)
public function setRouteResult(RouteResult $routeResult): void
{
$this->routeResult = $routeResult;
}

/**
* @return bool
*/
public function getIsActiveRecursion(): bool
{
return $this->isActiveRecursion;
}

/**
* @param $isActiveRecursion
*/
public function setIsActiveRecursion(bool $isActiveRecursion)
public function setIsActiveRecursion(bool $isActiveRecursion): void
{
if ($isActiveRecursion != $this->isActiveRecursion) {
if ($isActiveRecursion !== $this->isActiveRecursion) {
$this->isActiveRecursion = $isActiveRecursion;
$this->isActiveCache = array();
$this->isActiveCache = [];
}
}

/**
* @param string $name
* @return NavigationContainer
*/
public function getIsActiveCache(): array
{
return $this->isActiveCache;
}

public function getHrefCache(): array
{
return $this->hrefCache;
}

public function getContainer(string $name): NavigationContainer
{
if (isset($this->containers[$name])) {
return $this->containers[$name];
}

$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) {
41 changes: 9 additions & 32 deletions src/Service/NavigationInterface.php
Original file line number Diff line number Diff line change
@@ -1,55 +1,32 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\Service;

use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Page;
use Mezzio\Router\RouteResult;

/**
* Interface NavigationInterface
* @package Dot\Navigation\Service
*/
interface NavigationInterface
{
/**
* @param RouteResult $routeResult
*/
public function getRouteResult(): ?RouteResult;

public function setRouteResult(RouteResult $routeResult);

/**
* @param $isActiveRecursion
*/
public function getIsActiveRecursion(): bool;

public function setIsActiveRecursion(bool $isActiveRecursion);

/**
* @param $name
* @return NavigationContainer
*/
public function getIsActiveCache(): array;

public function getHrefCache(): array;

public function getContainer(string $name): NavigationContainer;

/**
* @param Page $page
* @return bool
*/
public function isAllowed(Page $page): bool;

/**
* @param Page $page
* @return bool
*/
public function isActive(Page $page): bool;

/**
* @param Page $page
* @return string
*/
public function getHref(Page $page): string;
}
91 changes: 30 additions & 61 deletions src/View/AbstractNavigationRenderer.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\View;

@@ -15,47 +10,44 @@
use Laminas\Escaper\Escaper;
use Mezzio\Template\TemplateRendererInterface;

/**
* Class AbstractNavigationHelper
* @package Dot\Navigation\Helper
*/
use function implode;
use function in_array;
use function is_array;
use function is_scalar;
use function is_string;
use function json_encode;
use function preg_match;
use function str_contains;
use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function strlen;
use function substr;
use function trim;

abstract class AbstractNavigationRenderer implements RendererInterface
{
/** @var NavigationInterface */
protected $navigation;

/** @var string */
protected $partial;

/** @var TemplateRendererInterface */
protected $template;
protected NavigationInterface $navigation;
protected TemplateRendererInterface $template;
protected string $partial;

/**
* AbstractNavigationHelper constructor.
* @param NavigationInterface $navigation
* @param TemplateRendererInterface $template
*/
public function __construct(NavigationInterface $navigation, TemplateRendererInterface $template)
{
$this->navigation = $navigation;
$this->template = $template;
$this->template = $template;
}

/**
* @param array $attributes
* @return string
*/
public function htmlAttributes(array $attributes): string
{
$xhtml = '';
$xhtml = '';
$escaper = new Escaper();

foreach ($attributes as $key => $val) {
$key = $escaper->escapeHtml($key);

if (('on' == substr($key, 0, 2)) || ('constraints' == $key)) {
if ((str_starts_with($key, 'on')) || ('constraints' === $key)) {
// Don't escape event attributes; _do_ substitute double quotes with singles
if (!is_scalar($val)) {
if (! is_scalar($val)) {
// non-scalar data should be cast to JSON first
$val = json_encode($val);
}
@@ -67,10 +59,10 @@ public function htmlAttributes(array $attributes): string

$val = $escaper->escapeHtmlAttr($val);

if ('id' == $key) {
if ('id' === $key) {
$val = $this->normalizeId($val);
}
if (strpos($val, '"') !== false) {
if (str_contains($val, '"')) {
$xhtml .= " $key='$val'";
} else {
$xhtml .= " $key=\"$val\"";
@@ -79,16 +71,10 @@ public function htmlAttributes(array $attributes): string
return $xhtml;
}

/**
* Normalize an ID
*
* @param string $value
* @return string
*/
protected function normalizeId(string $value): string
{
if (strstr($value, '[')) {
if ('[]' == substr($value, -2)) {
if (str_contains($value, '[')) {
if (str_ends_with($value, '[]')) {
$value = substr($value, 0, strlen($value) - 2);
}
$value = trim($value, ']');
@@ -98,44 +84,27 @@ protected function normalizeId(string $value): string
return $value;
}

/**
* @return string|null
*/
public function getPartial(): ?string
{
return $this->partial;
}

/**
* @param string $partial
*/
public function setPartial(string $partial)
public function setPartial(string $partial): void
{
$this->partial = $partial;
}

/**
* @param string|NavigationContainer $container
* @return NavigationContainer
*/
protected function getContainer($container): NavigationContainer
protected function getContainer(string|NavigationContainer $container): NavigationContainer
{
if (is_string($container)) {
return $this->navigation->getContainer($container);
} elseif (!$container instanceof NavigationContainer) {
} elseif (! $container instanceof NavigationContainer) {
throw new RuntimeException('Container must be a string or an instance of ' . NavigationContainer::class);
}

return $container;
}

/**
* Cleans array of attributes based on valid input.
*
* @param array $input
* @param array $valid
* @return array
*/
protected function cleanAttributes(array $input, array $valid): array
{
foreach ($input as $key => $value) {
53 changes: 17 additions & 36 deletions src/View/NavigationRenderer.php
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\View;

use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Service\Navigation;
use Dot\Navigation\Service\NavigationInterface;
use Mezzio\Template\TemplateRendererInterface;

/**
* Class NavigationRenderer
* @package Dot\Navigation\View
*/
use function array_merge;

class NavigationRenderer extends AbstractNavigationRenderer
{
/**
* @var NavigationOptions
*/
protected $options;
protected NavigationOptions $options;

/**
* NavigationMenu constructor.
* @param Navigation $navigation
* @param TemplateRendererInterface $template
* @param NavigationOptions $options
*/
public function __construct(
Navigation $navigation,
NavigationInterface $navigation,
TemplateRendererInterface $template,
NavigationOptions $options
) {
$this->options = $options;
parent::__construct($navigation, $template);
}

/**
* @param string $partial
* @param string|NavigationContainer $container
* @param array $params
* @return string
*/
public function renderPartial($container, string $partial, array $params = []): string
public function renderPartial(string|NavigationContainer $container, string $partial, array $params = []): string
{
$container = $this->getContainer($container);

@@ -59,13 +37,16 @@ public function renderPartial($container, string $partial, array $params = []):
);
}

/**
* @param string|NavigationContainer $container
* @return string
*/
public function render($container): string
public function render(string|NavigationContainer $container, string $template, array $params = []): string
{
// TODO: render a default HTML menu structure
return '';
$container = $this->getContainer($container);

return $this->template->render(
$template,
array_merge(
['container' => $container, 'navigation' => $this->navigation],
$params
)
);
}
}
29 changes: 3 additions & 26 deletions src/View/RendererInterface.php
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
<?php
/**
* @see https://github.com/dotkernel/dot-navigation/ for the canonical source repository
* @copyright Copyright (c) 2017 Apidemia (https://www.apidemia.com)
* @license https://github.com/dotkernel/dot-navigation/blob/master/LICENSE.md MIT License
*/

declare(strict_types = 1);
declare(strict_types=1);

namespace Dot\Navigation\View;

use Dot\Navigation\NavigationContainer;

/**
* Interface RendererInterface
* @package Dot\Navigation\View
*/
interface RendererInterface
{
/**
* @param string|NavigationContainer $container
* @return string
*/
public function render($container): string;
public function render(string|NavigationContainer $container, string $template, array $params = []): string;

/**
* @param $partial
* @param array $params
* @param string|NavigationContainer $container
* @return string
*/
public function renderPartial($container, string $partial, array $params = []): string;
public function renderPartial(string|NavigationContainer $container, string $partial, array $params = []): string;

/**
* @param array $attributes
* @return string
*/
public function htmlAttributes(array $attributes): string;
}
67 changes: 67 additions & 0 deletions test/ConfigProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation;

use Dot\Navigation\ConfigProvider;
use Dot\Navigation\Factory\NavigationMiddlewareFactory;
use Dot\Navigation\Factory\NavigationOptionsFactory;
use Dot\Navigation\Factory\NavigationRendererFactory;
use Dot\Navigation\Factory\NavigationServiceFactory;
use Dot\Navigation\Factory\ProviderPluginManagerFactory;
use Dot\Navigation\NavigationMiddleware;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Provider\Factory;
use Dot\Navigation\Provider\FactoryInterface;
use Dot\Navigation\Provider\ProviderPluginManager;
use Dot\Navigation\Service\Navigation;
use Dot\Navigation\Service\NavigationInterface;
use Dot\Navigation\View\NavigationRenderer;
use Dot\Navigation\View\RendererInterface;
use PHPUnit\Framework\TestCase;

class ConfigProviderTest extends TestCase
{
protected array $config;

protected function setup(): void
{
$this->config = (new ConfigProvider())();
}

public function testHasDependencies(): void
{
$this->assertArrayHasKey('dependencies', $this->config);
}

public function testDependenciesHasFactories(): void
{
$this->assertArrayHasKey('factories', $this->config['dependencies']);

$factories = $this->config['dependencies']['factories'];
$this->assertArrayHasKey(Navigation::class, $factories);
$this->assertSame(NavigationServiceFactory::class, $factories[Navigation::class]);
$this->assertArrayHasKey(NavigationMiddleware::class, $factories);
$this->assertSame(NavigationMiddlewareFactory::class, $factories[NavigationMiddleware::class]);
$this->assertArrayHasKey(NavigationOptions::class, $factories);
$this->assertSame(NavigationOptionsFactory::class, $factories[NavigationOptions::class]);
$this->assertArrayHasKey(NavigationRenderer::class, $factories);
$this->assertSame(NavigationRendererFactory::class, $factories[NavigationRenderer::class]);
$this->assertArrayHasKey(ProviderPluginManager::class, $factories);
$this->assertSame(ProviderPluginManagerFactory::class, $factories[ProviderPluginManager::class]);
}

public function testDependenciesHasAliases(): void
{
$this->assertArrayHasKey('aliases', $this->config['dependencies']);

$aliases = $this->config['dependencies']['aliases'];
$this->assertArrayHasKey(FactoryInterface::class, $aliases);
$this->assertSame(Factory::class, $aliases[FactoryInterface::class]);
$this->assertArrayHasKey(NavigationInterface::class, $aliases);
$this->assertSame(Navigation::class, $aliases[NavigationInterface::class]);
$this->assertArrayHasKey(RendererInterface::class, $aliases);
$this->assertSame(NavigationRenderer::class, $aliases[RendererInterface::class]);
}
}
19 changes: 19 additions & 0 deletions test/Exception/InvalidArgumentExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Exception;

use Dot\Navigation\Exception\ExceptionInterface;
use Dot\Navigation\Exception\InvalidArgumentException;
use PHPUnit\Framework\TestCase;

class InvalidArgumentExceptionTest extends TestCase
{
public function testWillReturnCorrectInstances(): void
{
$exception = new InvalidArgumentException('test');
$this->assertInstanceOf(InvalidArgumentException::class, $exception);
$this->assertInstanceOf(ExceptionInterface::class, $exception);
}
}
24 changes: 24 additions & 0 deletions test/Exception/RuntimeExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Exception;

use Dot\Navigation\Exception\ExceptionInterface;
use Dot\Navigation\Exception\RuntimeException;
use PHPUnit\Framework\TestCase;

class RuntimeExceptionTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}

public function testWillReturnCorrectInstances(): void
{
$exception = new RuntimeException('test');
$this->assertInstanceOf(RuntimeException::class, $exception);
$this->assertInstanceOf(ExceptionInterface::class, $exception);
}
}
59 changes: 59 additions & 0 deletions test/Factory/NavigationMiddlewareFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Factory;

use Dot\Navigation\Factory\NavigationMiddlewareFactory;
use Dot\Navigation\NavigationMiddleware;
use Dot\Navigation\Service\NavigationInterface;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class NavigationMiddlewareFactoryTest extends TestCase
{
/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateMiddlewareWithoutNavigationInterface(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with(NavigationInterface::class)
->willReturn(false);

$this->expectExceptionMessage(NavigationMiddlewareFactory::MESSAGE_MISSING_NAVIGATION);
(new NavigationMiddlewareFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillCreateMiddleware(): void
{
$container = $this->createMock(ContainerInterface::class);
$navigation = $this->createMock(NavigationInterface::class);

$container->expects($this->once())
->method('has')
->with(NavigationInterface::class)
->willReturn(true);

$container->expects($this->once())
->method('get')
->with(NavigationInterface::class)
->willReturn($navigation);

$middleware = (new NavigationMiddlewareFactory())($container);
$this->assertInstanceOf(NavigationMiddleware::class, $middleware);
}
}
84 changes: 84 additions & 0 deletions test/Factory/NavigationOptionsFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Factory;

use Dot\Navigation\Factory\NavigationOptionsFactory;
use Dot\Navigation\Options\NavigationOptions;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class NavigationOptionsFactoryTest extends TestCase
{
/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationOptionsWithoutConfig(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(false);

$this->expectExceptionMessage(NavigationOptionsFactory::MESSAGE_MISSING_CONFIG);
(new NavigationOptionsFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationOptionsWithoutPackageConfig(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(true);

$container->expects($this->once())
->method('get')
->with('config')
->willReturn([]);

$this->expectExceptionMessage(NavigationOptionsFactory::MESSAGE_MISSING_PACKAGE_CONFIG);
(new NavigationOptionsFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillCreateNavigationOptions(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(true);

$container->expects($this->once())
->method('get')
->with('config')
->willReturn([
'dot_navigation' => [
'active_recursion' => true,
],
]);

$options = (new NavigationOptionsFactory())($container);
$this->assertInstanceOf(NavigationOptions::class, $options);
}
}
102 changes: 102 additions & 0 deletions test/Factory/NavigationRendererFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Factory;

use Dot\Navigation\Factory\NavigationRendererFactory;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Service\NavigationInterface;
use Dot\Navigation\View\NavigationRenderer;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class NavigationRendererFactoryTest extends TestCase
{
/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationRendererWithoutNavigationInterface(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with(NavigationInterface::class)
->willReturn(false);

$this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_NAVIGATION_INTERFACE);
(new NavigationRendererFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationRendererWithoutTemplateRendererInterface(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->method('has')->willReturnMap([
[NavigationInterface::class, true],
[TemplateRendererInterface::class, false],
]);

$this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_TEMPLATE_RENDERER);
(new NavigationRendererFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationRendererWithoutNavigationOptions(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->method('has')->willReturnMap([
[NavigationInterface::class, true],
[TemplateRendererInterface::class, true],
[NavigationOptions::class, false],
]);

$this->expectExceptionMessage(NavigationRendererFactory::MESSAGE_MISSING_NAVIGATION_OPTIONS);
(new NavigationRendererFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationRenderer(): void
{
$container = $this->createMock(ContainerInterface::class);
$navigation = $this->createMock(NavigationInterface::class);
$renderer = $this->createMock(TemplateRendererInterface::class);
$options = $this->createMock(NavigationOptions::class);

$container->method('has')->willReturnMap([
[NavigationInterface::class, true],
[TemplateRendererInterface::class, true],
[NavigationOptions::class, true],
]);

$container->method('get')->willReturnMap([
[NavigationInterface::class, $navigation],
[TemplateRendererInterface::class, $renderer],
[NavigationOptions::class, $options],
]);

$renderer = (new NavigationRendererFactory())($container);
$this->assertInstanceOf(NavigationRenderer::class, $renderer);
}
}
137 changes: 137 additions & 0 deletions test/Factory/NavigationServiceFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Factory;

use Dot\Authorization\AuthorizationInterface;
use Dot\Helpers\Route\RouteHelper;
use Dot\Navigation\Factory\NavigationServiceFactory;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Provider\ProviderPluginManager;
use Dot\Navigation\Service\Navigation;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class NavigationServiceFactoryTest extends TestCase
{
/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationServiceWithoutRouteHelper(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with(RouteHelper::class)
->willReturn(false);

$this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_ROUTE_HELPER);
(new NavigationServiceFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationServiceWithoutProviderPluginManager(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->method('has')->willReturnMap([
[RouteHelper::class, true],
[ProviderPluginManager::class, false],
]);

$this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_PLUGIN_MANAGER);
(new NavigationServiceFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateNavigationServiceWithoutNavigationOptions(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->method('has')->willReturnMap([
[RouteHelper::class, true],
[ProviderPluginManager::class, true],
[NavigationOptions::class, false],
]);

$this->expectExceptionMessage(NavigationServiceFactory::MESSAGE_MISSING_NAVIGATION_OPTIONS);
(new NavigationServiceFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillCreateNavigationServiceWithoutAuthorizationInterface(): void
{
$container = $this->createMock(ContainerInterface::class);
$navigation = $this->createMock(RouteHelper::class);
$renderer = $this->createMock(TemplateRendererInterface::class);
$options = $this->createMock(NavigationOptions::class);

$container->method('has')->willReturnMap([
[RouteHelper::class, true],
[ProviderPluginManager::class, true],
[NavigationOptions::class, true],
[AuthorizationInterface::class, false],
]);

$container->method('get')->willReturnMap([
[RouteHelper::class, $navigation],
[TemplateRendererInterface::class, $renderer],
[NavigationOptions::class, $options],
[AuthorizationInterface::class, null],
]);

$service = (new NavigationServiceFactory())($container);
$this->assertInstanceOf(Navigation::class, $service);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillCreateNavigationServiceWithAuthorizationInterface(): void
{
$container = $this->createMock(ContainerInterface::class);
$navigation = $this->createMock(RouteHelper::class);
$renderer = $this->createMock(TemplateRendererInterface::class);
$options = $this->createMock(NavigationOptions::class);
$authorization = $this->createMock(AuthorizationInterface::class);

$container->method('has')->willReturnMap([
[RouteHelper::class, true],
[ProviderPluginManager::class, true],
[NavigationOptions::class, true],
[AuthorizationInterface::class, true],
]);

$container->method('get')->willReturnMap([
[RouteHelper::class, $navigation],
[TemplateRendererInterface::class, $renderer],
[NavigationOptions::class, $options],
[AuthorizationInterface::class, $authorization],
]);

$service = (new NavigationServiceFactory())($container);
$this->assertInstanceOf(Navigation::class, $service);
}
}
111 changes: 111 additions & 0 deletions test/Factory/ProviderPluginManagerFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Factory;

use Dot\Navigation\Factory\ProviderPluginManagerFactory;
use Dot\Navigation\Provider\ProviderPluginManager;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class ProviderPluginManagerFactoryTest extends TestCase
{
/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateProviderPluginManagerWithoutConfig(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(false);

$this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_CONFIG);
(new ProviderPluginManagerFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateProviderPluginManagerWithoutPackageConfig(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(true);

$container->expects($this->once())
->method('get')
->with('config')
->willReturn([]);

$this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_PACKAGE_CONFIG);
(new ProviderPluginManagerFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillNotCreateProviderPluginManagerWithoutConfigProviderManager(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(true);

$container->expects($this->once())
->method('get')
->with('config')
->willReturn([
'dot_navigation' => [
'active_recursion' => true,
],
]);

$this->expectExceptionMessage(ProviderPluginManagerFactory::MESSAGE_MISSING_CONFIG_PROVIDER_MANAGER);
(new ProviderPluginManagerFactory())($container);
}

/**
* @throws Exception
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function testWillCreateProviderPluginManager(): void
{
$container = $this->createMock(ContainerInterface::class);

$container->expects($this->once())
->method('has')
->with('config')
->willReturn(true);

$container->expects($this->once())
->method('get')
->with('config')
->willReturn([
'dot_navigation' => [
'provider_manager' => [],
],
]);

$manager = (new ProviderPluginManagerFactory())($container);
$this->assertInstanceOf(ProviderPluginManager::class, $manager);
}
}
91 changes: 91 additions & 0 deletions test/Filter/IsAllowedFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Filter;

use Dot\Authorization\AuthorizationInterface;
use Dot\Helpers\Route\RouteHelper;
use Dot\Navigation\Filter\IsAllowedFilter;
use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Options\NavigationOptions;
use Dot\Navigation\Page;
use Dot\Navigation\Provider\FactoryInterface;
use Dot\Navigation\Service\Navigation;
use Dot\Navigation\Service\NavigationInterface;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use RecursiveIterator;

class IsAllowedFilterTest extends TestCase
{
/**
* @throws Exception
*/
public function testWillAcceptWithoutAuthorization(): void
{
$iterator = $this->createMock(RecursiveIterator::class);
$factory = $this->createMock(FactoryInterface::class);
$route = $this->createMock(RouteHelper::class);
$options = $this->createMock(NavigationOptions::class);

$navigation = new Navigation($factory, $route, $options, null);
$filter = $this->getMockBuilder(IsAllowedFilter::class)
->setConstructorArgs([$iterator, $navigation])
->onlyMethods(['current'])
->getMock();
$filter->method('current')->willReturn(new Page());

$this->assertTrue($filter->accept());
}

/**
* @throws Exception
*/
public function testWillAcceptWithAuthorization(): void
{
$iterator = $this->createMock(RecursiveIterator::class);
$factory = $this->createMock(FactoryInterface::class);
$route = $this->createMock(RouteHelper::class);
$options = $this->createMock(NavigationOptions::class);
$authorization = $this->createMock(AuthorizationInterface::class);

$authorization->expects($this->once())->method('isGranted')->willReturn(true);

$page = new Page();
$page->setOption('permission', 'test');
$page->setOption('roles', []);

$navigation = new Navigation($factory, $route, $options, $authorization);
$filter = $this->getMockBuilder(IsAllowedFilter::class)
->setConstructorArgs([$iterator, $navigation])
->onlyMethods(['current'])
->getMock();
$filter->method('current')->willReturn($page);

$this->assertTrue($filter->accept());
}

/**
* @throws Exception
*/
public function testGetChildren(): void
{
$navigation = $this->createMock(NavigationInterface::class);

$filter = new IsAllowedFilter(new NavigationContainer([new Page()]), $navigation);
$this->assertInstanceOf(IsAllowedFilter::class, $filter->getChildren());
}

/**
* @throws Exception
*/
public function testWillCreateFilter(): void
{
$iterator = $this->createMock(RecursiveIterator::class);
$navigation = $this->createMock(NavigationInterface::class);

$filter = new IsAllowedFilter($iterator, $navigation);
$this->assertInstanceOf(IsAllowedFilter::class, $filter);
}
}
213 changes: 213 additions & 0 deletions test/NavigationContainerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation;

use Dot\Navigation\NavigationContainer;
use Dot\Navigation\Page;
use PHPUnit\Framework\TestCase;

class NavigationContainerTest extends TestCase
{
protected int $count = 5;

public function testWillAddPages(): void
{
$navigationContainer = new NavigationContainer();
$this->assertFalse($navigationContainer->hasChildren());
$navigationContainer->addPages([
new Page(),
]);
$this->assertTrue($navigationContainer->hasChildren());
}

public function testWillAddPage(): void
{
$navigationContainer = new NavigationContainer();
$this->assertFalse($navigationContainer->hasChildren());
$navigationContainer->addPage(new Page());
$this->assertTrue($navigationContainer->hasChildren());
}

public function testWillReturnCurrentChild(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$currentPage = $navigationContainer->current();
$this->assertInstanceOf(Page::class, $currentPage);
$this->assertSame($pages[0]->getOption('opt'), $currentPage->getOption('opt'));
}

public function testWillReturnNextChild(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);
$navigationContainer->next();

$currentPage = $navigationContainer->current();
$this->assertInstanceOf(Page::class, $currentPage);
$this->assertSame($pages[1]->getOption('opt'), $currentPage->getOption('opt'));
}

public function testWillReturnCurrentKey(): void
{
$navigationContainer = new NavigationContainer();
$this->assertSame(0, $navigationContainer->key());
}

public function testWillReturnInvalidIfNoItems(): void
{
$navigationContainer = new NavigationContainer();
$this->assertFalse($navigationContainer->valid());
}

public function testWillReturnValidWhileIndexExists(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$this->assertTrue($navigationContainer->valid());
for ($i = 0; $i < $this->count; $i++) {
$this->assertTrue($navigationContainer->valid());
$navigationContainer->next();
}
$this->assertFalse($navigationContainer->valid());
}

public function testWillReturnFalseIfNotHasChildren(): void
{
$navigationContainer = new NavigationContainer();
$this->assertFalse($navigationContainer->hasChildren());
}

public function testWillReturnTrueIfHasChildren(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$this->assertTrue($navigationContainer->hasChildren());
}

public function testWillNotFindOneByNotExistingAttribute(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$result = $navigationContainer->findOneByAttribute('test', 'test');
$this->assertNull($result);
}

public function testWillFindOneByExistingAttribute(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$result = $navigationContainer->findOneByAttribute('attr', 'attr #0');
$this->assertInstanceOf(Page::class, $result);
$this->assertSame($pages[0]->getAttribute('attr'), $result->getAttribute('attr'));
}

public function testWillNotFindManyByNonExistingAttribute(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$results = $navigationContainer->findByAttribute('test', 'test');
$this->assertIsArray($results);
$this->assertEmpty($results);
}

public function testWillFindManyByExistingAttribute(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$results = $navigationContainer->findByAttribute('attr', 'attr #0');
$this->assertIsArray($results);
$this->assertInstanceOf(Page::class, $results[0]);
$this->assertSame($pages[0]->getAttribute('attr'), $results[0]->getAttribute('attr'));
}

public function testWillNotFindOneByNonExistingOption(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$result = $navigationContainer->findOneByOption('test', 'test');
$this->assertNull($result);
}

public function testWillFindOneByExistingOption(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$result = $navigationContainer->findOneByOption('opt', 'opt #0');
$this->assertInstanceOf(Page::class, $result);
$this->assertSame($pages[0]->getOption('opt'), $result->getOption('opt'));
}

public function testWillNotFindManyByNonExistingOption(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$results = $navigationContainer->findByOption('test', 'test');
$this->assertIsArray($results);
$this->assertEmpty($results);
}

public function testWillFindManyByExistingOption(): void
{
$pages = $this->getTestPages();

$navigationContainer = new NavigationContainer();
$navigationContainer->addPages($pages);

$results = $navigationContainer->findByOption('opt', 'opt #0');
$this->assertIsArray($results);
$this->assertInstanceOf(Page::class, $results[0]);
$this->assertSame($pages[0]->getAttribute('opt'), $results[0]->getAttribute('opt'));
}

/**
* @return Page[]
*/
protected function getTestPages(): array
{
$pages = [];

for ($i = 0; $i < $this->count; $i++) {
$page = new Page();
$page->setOption('opt', 'opt #' . $i);
$page->setAttribute('attr', 'attr #' . $i);
$pages[] = $page;
}

return $pages;
}
}
41 changes: 41 additions & 0 deletions test/NavigationMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation;

use Dot\Navigation\NavigationMiddleware;
use Dot\Navigation\Service\NavigationInterface;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NavigationMiddlewareTest extends TestCase
{
/**
* @throws Exception
*/
public function testWillCreateMiddleware(): void
{
$navigation = $this->createMock(NavigationInterface::class);

$middleware = new NavigationMiddleware($navigation);
$this->assertInstanceOf(NavigationMiddleware::class, $middleware);
}

/**
* @throws Exception
*/
public function testWillProcessRequest(): void
{
$navigation = $this->createMock(NavigationInterface::class);
$request = $this->createMock(ServerRequestInterface::class);
$handler = $this->createMock(RequestHandlerInterface::class);

$middleware = new NavigationMiddleware($navigation);
$response = $middleware->process($request, $handler);
$this->assertInstanceOf(ResponseInterface::class, $response);
}
}
30 changes: 30 additions & 0 deletions test/Options/NavigationOptionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DotTest\Navigation\Options;

use Dot\Navigation\Options\NavigationOptions;
use PHPUnit\Framework\TestCase;

class NavigationOptionsTest extends TestCase
{
public function testWillCreateNavigationOptions(): void
{
$options = new NavigationOptions();
$this->assertInstanceOf(NavigationOptions::class, $options);
}

public function testAccessors(): void
{
$options = new NavigationOptions();
$this->assertIsArray($options->getContainers());
$this->assertEmpty($options->getContainers());
$options->setContainers(['test']);
$this->assertIsArray($options->getContainers());
$this->assertCount(1, $options->getContainers());
$this->assertTrue($options->getActiveRecursion());
$options->setActiveRecursion(false);
$this->assertFalse($options->getActiveRecursion());
}
}
Loading

0 comments on commit 63b4506

Please sign in to comment.