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..377bcfb --- /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..593bed9 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,46 @@ +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 f3e6777..2f9a784 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties +.phpcs-cache +.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 774151f..8701d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 4.2.0 - 2023-08-14 + +### Changed +* Added unit tests & removed laminas/laminas-log dependency + +### Added +* Unit Tests + +### Deprecated +* Nothing + +### Removed +* laminas/laminas-log dependency + +### Fixed +* Decoupled packages from laminas/laminas-log + + ## 4.0.1 - 2022-05-31 ### Changed diff --git a/LICENSE.md b/LICENSE.md index 24e7185..6757d85 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Apidemia +Copyright (c) 2023 Apidemia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/OSSMETADATA b/OSSMETADATA index b96d4a4..58d43b8 100644 --- a/OSSMETADATA +++ b/OSSMETADATA @@ -1 +1 @@ -osslifecycle=active +osslifecycle=maintained diff --git a/README.md b/README.md index a782176..34e893b 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ DotKernel form component extending and customizing [laminas-form](https://github.com/laminas/laminas-form) ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-form) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-form/4.0.1) - - - +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-form/4.2.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-form)](https://github.com/dotkernel/dot-form/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-form)](https://github.com/dotkernel/dot-form/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-form)](https://github.com/dotkernel/dot-form/stargazers) [![GitHub license](https://img.shields.io/github/license/dotkernel/dot-form)](https://github.com/dotkernel/dot-form/blob/4.0/LICENSE.md) +[![Build Static](https://github.com/dotkernel/dot-form/actions/workflows/static-analysis.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-form/actions/workflows/static-analysis.yml) + +[![SymfonyInsight](https://insight.symfony.com/projects/370a5200-2e49-47da-9988-8e1de8f49502/big.svg)](https://insight.symfony.com/projects/370a5200-2e49-47da-9988-8e1de8f49502) diff --git a/composer.json b/composer.json index a100d06..e4d2e33 100644 --- a/composer.json +++ b/composer.json @@ -18,24 +18,40 @@ } ], "require": { - "php": "~7.4.0 || ~8.0.0 || ~8.1.0", + "php": "~8.1 || ~8.2", "laminas/laminas-servicemanager": "^3.10", - "laminas/laminas-form": "^3.1.1", - "laminas/laminas-log": "2.15.2" + "laminas/laminas-form": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.1", - "squizlabs/php_codesniffer": "^3.5", - "doctrine/annotations": "^1.10" + "phpunit/phpunit": "^10.2", + "laminas/laminas-coding-standard": "^2.5", + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { - "Dot\\Form\\": "src" + "Dot\\Form\\": "src/" } }, "autoload-dev": { "psr-4": { - "DotTest\\Form\\": "tests" + "DotTest\\Form\\": "test/" } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "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/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..75be41e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + ./test + + + + + + ./src + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..2015b2a --- /dev/null +++ b/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 99d7daf..c3e6c25 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,23 +1,13 @@ $this->getDependenciesConfig(), 'view_helpers' => $this->getViewHelpersConfig(), - - 'dot_form' => [ + 'dot_form' => [ 'forms' => [], - ], ]; } @@ -39,21 +27,18 @@ public function getDependenciesConfig(): array 'abstract_factories' => [ FormAbstractServiceFactory::class, ], - 'aliases' => [ - 'Laminas\Form\Annotation\FormAnnotationBuilder' => 'FormAnnotationBuilder', - AnnotationBuilder::class => 'FormAnnotationBuilder', + 'aliases' => [ FormElementManager::class => 'FormElementManager', ], - 'factories' => [ + 'factories' => [ 'FormElementManager' => FormElementManagerFactory::class, - 'FormAnnotationBuilder' => AnnotationBuilderFactory::class, - ] + ], ]; } public function getViewHelpersConfig(): array { - $laminasFormConfigProvider = new \Laminas\Form\ConfigProvider(); + $laminasFormConfigProvider = new LaminasConfigProvider(); return $laminasFormConfigProvider->getViewHelperConfig(); } } diff --git a/src/Factory/FormAbstractServiceFactory.php b/src/Factory/FormAbstractServiceFactory.php index 80cb9db..6d787bc 100644 --- a/src/Factory/FormAbstractServiceFactory.php +++ b/src/Factory/FormAbstractServiceFactory.php @@ -1,94 +1,107 @@ getConfig($container); + if (empty($config)) { + return false; + } + + return ! empty($config[$requestedName]) && is_array($config[$requestedName]); } /** - * @param ContainerInterface $container * @param string $requestedName - * @param array|null $options - * @return \Laminas\Form\ElementInterface + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): FormInterface { - $parts = explode('.', $requestedName); - - //merge configs if extends another form - $config = $this->getConfig($container); - $specificConfig = $config[$parts[1]]; + $config = $this->getConfig($container); + $config = $config[$requestedName]; + $factory = $this->getFormFactory($container); - do { - $extendsConfigKey = isset($specificConfig['extends']) && is_string($specificConfig['extends']) - ? trim($specificConfig['extends']) - : null; - - unset($specificConfig['extends']); - - if (!is_null($extendsConfigKey) - && array_key_exists($extendsConfigKey, $config) - && is_array($config[$extendsConfigKey]) - ) { - $specificConfig = ArrayUtils::merge($config[$extendsConfigKey], $specificConfig); - } - } while ($extendsConfigKey != null); - - $this->config[$parts[1]] = $specificConfig; - - return parent::__invoke($container, $parts[1], $options); + $this->marshalInputFilter($config, $container, $factory); + return $factory->createForm($config); } /** - * @param ContainerInterface $container - * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ protected function getConfig(ContainerInterface $container): array { - parent::getConfig($container); - if (!empty($this->config)) { + if ($this->config !== null) { + return $this->config; + } + + if (! $container->has('config')) { + $this->config = []; + return $this->config; + } + + $config = $container->get('config'); + if (! isset($config[$this->configKey]) || ! is_array($config[$this->configKey])) { + $this->config = []; + return $this->config; + } + + $this->config = $config[$this->configKey]; + + if (! empty($this->config)) { if (isset($this->config[$this->subConfigKey]) && is_array($this->config[$this->subConfigKey])) { $this->config = $this->config[$this->subConfigKey]; } @@ -97,28 +110,30 @@ protected function getConfig(ContainerInterface $container): array return $this->config; } - protected function getFormFactory(ContainerInterface $container): \Laminas\Form\Factory + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function getFormFactory(ContainerInterface $container): FormFactory { - $formFactory = parent::getFormFactory($container); - if ($container->has('InputFilterManager')) { - $formFactory->setInputFilterFactory(new Factory($container->get('InputFilterManager'))); + if ($this->factory instanceof Factory) { + return $this->factory; + } + + $elements = null; + if ($container->has(FormElementManager::class)) { + $elements = $container->get(FormElementManager::class); } - return $formFactory; + $this->factory = new Factory($elements); + return $this->factory; } /** - * Marshal the input filter into the configuration - * - * If an input filter is specified: - * - if the InputFilterManager is present, checks if it's there; if so, - * retrieves it and resets the specification to the instance. - * - otherwise, pulls the input filter factory from the form factory, and - * attaches the FilterManager and ValidatorManager to it. - * - * @param array $config + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface */ - protected function marshalInputFilter(array &$config, ContainerInterface $container, \Laminas\Form\Factory $formFactory): void + protected function marshalInputFilter(array &$config, ContainerInterface $container, FormFactory $formFactory): void { if (! isset($config['input_filter'])) { return; @@ -128,11 +143,8 @@ protected function marshalInputFilter(array &$config, ContainerInterface $contai return; } - if ( - is_string($config['input_filter']) - && $container->has('InputFilterManager') - ) { - $inputFilters = $container->get('InputFilterManager'); + if (is_string($config['input_filter']) && $container->has(InputFilterPluginManager::class)) { + $inputFilters = $container->get(InputFilterPluginManager::class); if ($inputFilters->has($config['input_filter'])) { $config['input_filter'] = $inputFilters->get($config['input_filter']); return; @@ -140,7 +152,14 @@ protected function marshalInputFilter(array &$config, ContainerInterface $contai } $inputFilterFactory = $formFactory->getInputFilterFactory(); - $inputFilterFactory->getDefaultFilterChain()->setPluginManager($container->get('FilterManager')); - $inputFilterFactory->getDefaultValidatorChain()->setPluginManager($container->get('ValidatorManager')); + $filterChain = $inputFilterFactory->getDefaultFilterChain(); + $filterChain?->setPluginManager( + $container->get(FilterPluginManager::class) + ); + + $validatorChain = $inputFilterFactory->getDefaultValidatorChain(); + $validatorChain?->setPluginManager( + $container->get(ValidatorPluginManager::class) + ); } } diff --git a/src/Factory/FormElementManagerFactory.php b/src/Factory/FormElementManagerFactory.php index eac5cd2..d9687c3 100644 --- a/src/Factory/FormElementManagerFactory.php +++ b/src/Factory/FormElementManagerFactory.php @@ -1,24 +1,21 @@ get('config')['dot_form']['form_manager']); } diff --git a/src/FormElementManager.php b/src/FormElementManager.php index 2ff850e..0ee7e86 100644 --- a/src/FormElementManager.php +++ b/src/FormElementManager.php @@ -1,19 +1,14 @@ config = (new ConfigProvider())(); + } + + public function testHasDependencies(): void + { + $this->assertArrayHasKey('dependencies', $this->config); + } + + public function testDependenciesHasAbstractFactories(): void + { + $this->assertArrayHasKey('abstract_factories', $this->config['dependencies']); + $this->assertContains( + FormAbstractServiceFactory::class, + $this->config['dependencies']['abstract_factories'] + ); + } + + public function testDependenciesHasAliases(): void + { + $this->assertArrayHasKey('aliases', $this->config['dependencies']); + $this->assertArrayHasKey(FormElementManager::class, $this->config['dependencies']['aliases']); + } + + public function testDependenciesHasFactories(): void + { + $this->assertArrayHasKey('factories', $this->config['dependencies']); + $this->assertArrayHasKey('FormElementManager', $this->config['dependencies']['factories']); + } +} diff --git a/test/Factory/FormAbstractServiceFactoryTest.php b/test/Factory/FormAbstractServiceFactoryTest.php new file mode 100644 index 0000000..927a0ff --- /dev/null +++ b/test/Factory/FormAbstractServiceFactoryTest.php @@ -0,0 +1,256 @@ +subject = new FormAbstractServiceFactory(); + $this->container = $this->createMock(ContainerInterface::class); + } + + public function testClassImplementsAbstractFactoryInterface(): void + { + $this->assertInstanceOf(AbstractFactoryInterface::class, $this->subject); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testCanCreateReturnsFalseInvalidFormName(): void + { + $requestedName = 'invalidName'; + + $canCreate = $this->subject->canCreate($this->container, $requestedName); + $this->assertFalse($canCreate); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testCanCreateReturnsFalseMissingPrefix(): void + { + $requestedName = 'dot.form'; + + $canCreate = $this->subject->canCreate($this->container, $requestedName); + $this->assertFalse($canCreate); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testCanCreateReturnsFalseConfigProvided(): void + { + $requestedName = 'config'; + + $canCreate = $this->subject->canCreate($this->container, $requestedName); + $this->assertFalse($canCreate); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testCanCreateReturnsFalseEmptyConfig(): void + { + $requestedName = 'dot-form.form'; + $container = $this->createMock(ContainerInterface::class); + + $container->method('get')->willReturn([]); + + $canCreate = $this->subject->canCreate($container, $requestedName); + $this->assertFalse($canCreate); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testCanCreateReturnsFalseInvalidConfig(): void + { + $requestedName = 'dot-form.form'; + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once())->method('has')->willReturn(true); + $container->method('get')->willReturn(['dot_form' => []]); + + $canCreate = $this->subject->canCreate($container, $requestedName); + $this->assertFalse($canCreate); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testCanCreateReturnsTrue(): void + { + $requestedName = 'dot-form.form'; + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturn(true); + $container->method('get')->willReturn(['dot_form' => ['dot-form.form' => ['form_name']]]); + + $canCreate = $this->subject->canCreate($container, $requestedName); + $this->assertTrue($canCreate); + } + + /** + * @throws ReflectionException + */ + public function testGetExistingConfig(): void + { + $config = ['form.dot_forms']; + $reflectionClass = new ReflectionClass(FormAbstractServiceFactory::class); + $reflectionClass->getProperty('config')->setValue($this->subject, $config); + + $result = $reflectionClass + ->getMethod('getConfig') + ->invoke($this->subject, $this->container); + + $this->assertSame($config, $result); + } + + /** + * @throws ReflectionException + */ + public function testConfigNotFound(): void + { + $this->container->expects($this->once())->method('has')->willReturn(false); + + $config = $this->callMethod($this->subject, 'getConfig', $this->container); + + $this->assertSame([], $config); + } + + /** + * @throws ReflectionException + */ + public function testGetInvalidConfig(): void + { + $this->container->expects($this->once())->method('has')->willReturn(true); + $this->container->expects($this->once())->method('get')->willReturn(['form']); + + $config = $this->callMethod($this->subject, 'getConfig', $this->container); + + $this->assertSame([], $config); + } + + /** + * @throws ReflectionException + */ + public function testGetValidConfig(): void + { + $config = ['dot_form' => ['form_name']]; + $this->container->expects($this->once())->method('has')->willReturn(true); + $this->container->expects($this->once())->method('get')->willReturn($config); + + $result = $this->callMethod($this->subject, 'getConfig', $this->container); + + $this->assertSame($config['dot_form'], $result); + } + + /** + * @throws ReflectionException + */ + public function testGetConfigSubKey(): void + { + $config = ['dot_form' => ['forms' => ['form_name']]]; + $this->container->expects($this->once())->method('has')->willReturn(true); + $this->container->expects($this->once())->method('get')->willReturn($config); + + $result = $this->callMethod($this->subject, 'getConfig', $this->container); + + $this->assertSame($config['dot_form']['forms'], $result); + } + + /** + * @throws Exception + * @throws ReflectionException + */ + public function testGetExistingFormFactory(): void + { + $formFactory = $this->createMock(Factory::class); + $reflectionClass = new ReflectionClass(FormAbstractServiceFactory::class); + $reflectionClass->getProperty('factory')->setValue($this->subject, $formFactory); + + $result = $reflectionClass + ->getMethod('getFormFactory') + ->invoke($this->subject, $this->container); + + $this->assertInstanceOf(Factory::class, $result); + } + + /** + * @throws ReflectionException + */ + public function testGetFormFactoryFormElementManagerNotFound(): void + { + $this->container->expects($this->once())->method('has')->willReturn(false); + + $result = $this->callMethod($this->subject, 'getFormFactory', $this->container); + $reflectionClass = new ReflectionClass($result); + + $this->assertInstanceOf(Factory::class, $result); + $this->assertNull($reflectionClass->getProperty('formElementManager')->getValue($result)); + } + + /** + * @throws Exception + * @throws ReflectionException + */ + public function testGetFormFactoryExistingFormElementManager(): void + { + $formElementManager = $this->createMock(FormElementManager::class); + $this->container->expects($this->once())->method('has')->willReturn(true); + $this->container->expects($this->once())->method('get')->willReturn($formElementManager); + + $result = $this->callMethod($this->subject, 'getFormFactory', $this->container); + $reflectionClass = new ReflectionClass($result); + + $this->assertInstanceOf(Factory::class, $result); + $this->assertInstanceOf( + FormElementManager::class, + $reflectionClass->getProperty('formElementManager')->getValue($result) + ); + } + + /** + * @throws ReflectionException + */ + private function callMethod(object $object, string $method, mixed ...$args): mixed + { + $reflectionClass = new ReflectionClass($object::class); + + return $reflectionClass->getMethod($method)->invoke($object, ...$args); + } +} diff --git a/test/Factory/FormElementManagerFactoryTest.php b/test/Factory/FormElementManagerFactoryTest.php new file mode 100644 index 0000000..e5482d5 --- /dev/null +++ b/test/Factory/FormElementManagerFactoryTest.php @@ -0,0 +1,35 @@ +createMock(ContainerInterface::class); + $container->expects($this->once())->method('get')->willReturn([ + 'dot_form' => [ + 'form_manager' => [], + ], + ]); + + $formElementManagerFactory = (new FormElementManagerFactory())($container); + + $this->assertInstanceOf(FormElementManager::class, $formElementManagerFactory); + } +}