From 0de901d45c485cfcda1f656c3ba0d2bdd1d56255 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sat, 1 Jun 2024 23:15:35 +0200 Subject: [PATCH 1/8] First working version --- .woodpecker/code-style.yml | 10 ++++ .woodpecker/functional-tests.yml | 34 +++++++++++ Classes/InvokeRenderFunctionInterface.php | 10 ++++ Classes/Utils/DummyView.php | 30 ++++++++++ Classes/Utils/GeneratorClosureIterator.php | 21 +++++++ Classes/Utils/NodeRenderTransfer.php | 35 +++++++++++ Classes/Utils/RenderableProxy.php | 39 ++++++++++++ .../ApplyRenderFunctionViewHelper.php | 48 +++++++++++++++ .../RegisterRenderFunctionViewHelper.php | 54 +++++++++++++++++ License.txt | 19 ++++++ README.md | 17 ++++++ Resources/Private/Layouts/Test.html | 1 + .../Tests/Functional/Fixtures/Test/Index.html | 8 +++ .../Functional/Fixtures/Test/Select.html | 8 +++ .../Fixtures/Controller/TestController.php | 22 +++++++ Tests/Functional/RenderFunctionsTest.php | 59 +++++++++++++++++++ composer.json | 45 ++++++++++++++ 17 files changed, 460 insertions(+) create mode 100644 .woodpecker/code-style.yml create mode 100644 .woodpecker/functional-tests.yml create mode 100644 Classes/InvokeRenderFunctionInterface.php create mode 100644 Classes/Utils/DummyView.php create mode 100644 Classes/Utils/GeneratorClosureIterator.php create mode 100644 Classes/Utils/NodeRenderTransfer.php create mode 100644 Classes/Utils/RenderableProxy.php create mode 100644 Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php create mode 100644 Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php create mode 100644 License.txt create mode 100644 README.md create mode 100644 Resources/Private/Layouts/Test.html create mode 100644 Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html create mode 100644 Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html create mode 100644 Tests/Functional/Fixtures/Controller/TestController.php create mode 100644 Tests/Functional/RenderFunctionsTest.php create mode 100644 composer.json diff --git a/.woodpecker/code-style.yml b/.woodpecker/code-style.yml new file mode 100644 index 0000000..6f13ed1 --- /dev/null +++ b/.woodpecker/code-style.yml @@ -0,0 +1,10 @@ +steps: + code-style: + image: composer + commands: + - composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer + - composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + - composer global require digicomp/php-codesniffer:@dev + - composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/ + when: + - event: [push, pull_request, manual] diff --git a/.woodpecker/functional-tests.yml b/.woodpecker/functional-tests.yml new file mode 100644 index 0000000..2dbb2a3 --- /dev/null +++ b/.woodpecker/functional-tests.yml @@ -0,0 +1,34 @@ +workspace: + base: /woodpecker + path: package + +matrix: + include: + - FLOW_VERSION: 6.3 + PHP_VERSION: 7.4 + - FLOW_VERSION: 7.3 + PHP_VERSION: 7.4 + - FLOW_VERSION: 7.3 + PHP_VERSION: 8.2 + - FLOW_VERSION: 8.2 + PHP_VERSION: 8.2 + +steps: + functional-tests: + image: "thecodingmachine/php:${PHP_VERSION}-v4-cli" + environment: + # Enable the PDO_SQLITE extension + - "PHP_EXTENSION_PDO_SQLITE=1" + - "FLOW_VERSION=${FLOW_VERSION}" + - "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}" + commands: + - "sudo mkdir $NEOS_BUILD_DIR" + - "sudo chown -R docker:docker $NEOS_BUILD_DIR" + - "cd $NEOS_BUILD_DIR" + - "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ." + - "composer config repositories.repo-name path /woodpecker/package" + - "composer remove --dev --no-update neos/behat || composer remove --no-update neos/behat" + - "composer require digicomp/fluid-render-functions:@dev" + - "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FluidRenderFunctions/Tests/Functional" + when: + - event: [ push, pull_request, manual ] diff --git a/Classes/InvokeRenderFunctionInterface.php b/Classes/InvokeRenderFunctionInterface.php new file mode 100644 index 0000000..01554e2 --- /dev/null +++ b/Classes/InvokeRenderFunctionInterface.php @@ -0,0 +1,10 @@ +closure = $closure; + } + + public function getIterator(): \Generator + { + return ($this->closure)(); + } +} diff --git a/Classes/Utils/NodeRenderTransfer.php b/Classes/Utils/NodeRenderTransfer.php new file mode 100644 index 0000000..068d605 --- /dev/null +++ b/Classes/Utils/NodeRenderTransfer.php @@ -0,0 +1,35 @@ +node = $node; + $this->subjectName = $subjectName; + } + + public function __invoke($object): string + { + $context = new RenderingContext(new DummyView()); + $context->getVariableProvider()->add($this->subjectName, $object); + return \trim($this->node->evaluate($context)); + } +} diff --git a/Classes/Utils/RenderableProxy.php b/Classes/Utils/RenderableProxy.php new file mode 100644 index 0000000..2ab78de --- /dev/null +++ b/Classes/Utils/RenderableProxy.php @@ -0,0 +1,39 @@ +renderFunction = $renderFunction; + $this->object = $object; + try { + $this->Persistence_Object_Identifier = + ObjectAccess::getProperty($object, 'Persistence_Object_Identifier', true); + } catch (PropertyNotAccessibleException $e) { + // ok. fine + } + } + + public function __toString(): string + { + $render = $this->renderFunction; + return $render($this->object); + } +} diff --git a/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php b/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php new file mode 100644 index 0000000..cf97859 --- /dev/null +++ b/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php @@ -0,0 +1,48 @@ +registerArgument('in', 'mixed', 'subject to apply the render function to'); + $this->registerArgument('function', InvokeRenderFunctionInterface::class, 'render function to use', true); + $this->registerArgument( + 'force', + 'bool', + 'if set, it will be applied to the provided in, if not, it will be applied to each item for ' + . 'iterables instead', + false, + false + ); + } + + public function render() + { + $in = $this->arguments['in']; + if ($in === null) { + $in = $this->renderChildren(); + } + if (\is_iterable($in) && $this->arguments['force'] === false) { + return new GeneratorClosureIterator(fn () => $this->getProxyGenerator($this->arguments['function'], $in)); + } else { + return new RenderableProxy($this->arguments['function'], $in); + } + } + + protected function getProxyGenerator(InvokeRenderFunctionInterface $function, iterable $objects): \Generator + { + foreach ($objects as $object) { + yield new RenderableProxy($function, $object); + } + } +} diff --git a/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php b/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php new file mode 100644 index 0000000..5db7772 --- /dev/null +++ b/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php @@ -0,0 +1,54 @@ +registerArgument('as', 'string', 'Name of the registered render function', false, 'renderFunc'); + $this->registerArgument( + 'subjectName', + 'string', + 'Name of the argument passed to render function', + false, + 'subject' + ); + } + + public function compile( + $argumentsName, + $closureName, + &$initializationPhpCode, + ViewHelperNode $node, + TemplateCompiler $compiler + ) { + // we disable compiling, because we will need access to the AST, which is not available if compiled + // a cool improvement would be to drop the need to the AST and so become compilable + $compiler->disable(); + return "''"; + } + + public function render(): string + { + $transferNode = new RootNode(); + foreach ($this->childNodes as $childNode) { + $transferNode->addChildNode($childNode); + } + $this->renderingContext->getVariableProvider() + ->add($this->arguments['as'], new NodeRenderTransfer($transferNode, $this->arguments['subjectName'])); + return ''; + } +} diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..7c439ae --- /dev/null +++ b/License.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024 Ferdinand Kuhl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dc2ced --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# DigiComp.FluidRenderFunctions + +This Package provides you with the possibility to register render functions, created from them, to use them dynamically else where. + +Let me show you the idea: +```html + +``` +Assuming you know how the SelectViewHelper works, you know, you can provide an "optionLabelField"-argument to adivce the ViewHelper to use a property of your options. +But, what if you want to use a complete template, to display your books? +FluidRenderFunctions to the rescue: +```html + + {myBook.name} from {myBook.author.name} + + +``` diff --git a/Resources/Private/Layouts/Test.html b/Resources/Private/Layouts/Test.html new file mode 100644 index 0000000..24f730c --- /dev/null +++ b/Resources/Private/Layouts/Test.html @@ -0,0 +1 @@ + diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html new file mode 100644 index 0000000..369d8a4 --- /dev/null +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html @@ -0,0 +1,8 @@ + +{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers} + + + {subject.name} is cool + + {test -> rf:applyRenderFunction(function: testFunc, force: true)} + diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html new file mode 100644 index 0000000..1899cee --- /dev/null +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html @@ -0,0 +1,8 @@ + +{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers} + + + {subject.name} is cool + + + diff --git a/Tests/Functional/Fixtures/Controller/TestController.php b/Tests/Functional/Fixtures/Controller/TestController.php new file mode 100644 index 0000000..f86c2b7 --- /dev/null +++ b/Tests/Functional/Fixtures/Controller/TestController.php @@ -0,0 +1,22 @@ +view->assign('test', ['name' => 'hallo']); + } + + public function selectAction() + { + $this->view->assign('testEntities', (new Query(TestEntity::class))->execute()); + } +} diff --git a/Tests/Functional/RenderFunctionsTest.php b/Tests/Functional/RenderFunctionsTest.php new file mode 100644 index 0000000..86acbb6 --- /dev/null +++ b/Tests/Functional/RenderFunctionsTest.php @@ -0,0 +1,59 @@ +setUriPattern('test/fluidrenderfunctions/test(/{@action})'); + $route->setDefaults([ + '@package' => 'DigiComp.FluidRenderFunctions', + '@subpackage' => 'Tests\Functional\Fixtures', + '@controller' => 'Test', + '@action' => 'index', + ]); + $route->setAppendExceedingArguments(true); + $this->router->addRoute($route); + } + + /** + * @test + */ + public function itAllowsFluidToRenderWrappedArray(): void + { + $response = $this->browser->request('http://localhost/test/fluidrenderfunctions/test'); + static::assertEquals("hallo is cool\n", (string)$response->getBody()); + } + + /** + * @test + */ + public function itAllowsToRenderTheOptionsArgumentOfSelectNicely(): void + { + $testEntity1 = new TestEntity(new ArrayCollection(), 'hallo'); + $this->persistenceManager->add($testEntity1); + $testEntity2 = new TestEntity(new ArrayCollection(), 'hallo 2'); + $this->persistenceManager->add($testEntity2); + $this->persistenceManager->persistAll(); + + $response = $this->browser->request('http://localhost/test/fluidrenderfunctions/test/select'); + static::assertStringContainsString('hallo is cool', (string)$response->getBody()); + static::assertStringContainsString('hallo 2 is cool', (string)$response->getBody()); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f9877ca --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "digicomp/fluid-render-functions", + "description": "Adapter for datatables", + "type": "neos-package", + "require": { + "neos/flow": "^6.3.5 | ^7.3 | ^8.3", + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "~8.5" + }, + "autoload": { + "psr-4": { + "DigiComp\\FluidRenderFunctions\\": "Classes/" + } + }, + "autoload-dev": { + "psr-4": { + "DigiComp\\FluidRenderFunctions\\Tests\\": "Tests/" + } + }, + "extra": { + "neos": { + "package-key": "DigiComp.FluidRenderFunctions" + }, + "branch-alias": { + "dev-develop": "1.0.x-dev" + } + }, + "authors": [ + { + "name": "Ferdinand Kuhl", + "email": "f.kuhl@digital-competence.de", + "homepage": "https://www.digital-competence.de", + "role": "Developer" + } + ], + "license": "MIT", + "homepage": "https://git.digital-competence.de/Packages/DigiComp.FluidRenderFunctions", + "keywords": [ + "Neos", + "Flow", + "fluid" + ] +} From d9a71f8aa74919aa7f6a1992fe8912d1d2981744 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sat, 1 Jun 2024 23:20:41 +0200 Subject: [PATCH 2/8] fixing description and adding depency to fluid-adaptor --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f9877ca..4e1cc21 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,10 @@ { "name": "digicomp/fluid-render-functions", - "description": "Adapter for datatables", + "description": "define fluid based render functions and use them else where", "type": "neos-package", "require": { "neos/flow": "^6.3.5 | ^7.3 | ^8.3", + "neos/fluid-adaptor": "^6.3.5 | ^7.3 | ^8.3", "php": ">=7.4" }, "require-dev": { From 713941b6884f4549eb82694b21e35203db4daa10 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sat, 1 Jun 2024 23:24:43 +0200 Subject: [PATCH 3/8] replacing test entities from DataTablesAdapter with entities from FluidAdaptor --- .../Functional/Fixtures/Controller/TestController.php | 4 ++-- Tests/Functional/RenderFunctionsTest.php | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/Functional/Fixtures/Controller/TestController.php b/Tests/Functional/Fixtures/Controller/TestController.php index f86c2b7..64f75b0 100644 --- a/Tests/Functional/Fixtures/Controller/TestController.php +++ b/Tests/Functional/Fixtures/Controller/TestController.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace DigiComp\FluidRenderFunctions\Tests\Functional\Fixtures\Controller; -use DigiComp\FlowDataTablesAdapter\Tests\Functional\Fixtures\TestEntity; use Neos\Flow\Mvc\Controller\ActionController; use Neos\Flow\Persistence\Doctrine\Query; +use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag; class TestController extends ActionController { @@ -17,6 +17,6 @@ class TestController extends ActionController public function selectAction() { - $this->view->assign('testEntities', (new Query(TestEntity::class))->execute()); + $this->view->assign('testEntities', (new Query(Tag::class))->execute()); } } diff --git a/Tests/Functional/RenderFunctionsTest.php b/Tests/Functional/RenderFunctionsTest.php index 86acbb6..6452834 100644 --- a/Tests/Functional/RenderFunctionsTest.php +++ b/Tests/Functional/RenderFunctionsTest.php @@ -4,10 +4,9 @@ declare(strict_types=1); namespace DigiComp\FluidRenderFunctions\Tests\Functional; -use DigiComp\FlowDataTablesAdapter\Tests\Functional\Fixtures\TestEntity; -use Doctrine\Common\Collections\ArrayCollection; use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; +use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag; class RenderFunctionsTest extends FunctionalTestCase { @@ -46,10 +45,10 @@ class RenderFunctionsTest extends FunctionalTestCase */ public function itAllowsToRenderTheOptionsArgumentOfSelectNicely(): void { - $testEntity1 = new TestEntity(new ArrayCollection(), 'hallo'); - $this->persistenceManager->add($testEntity1); - $testEntity2 = new TestEntity(new ArrayCollection(), 'hallo 2'); - $this->persistenceManager->add($testEntity2); + $post1 = new Tag('hallo'); + $this->persistenceManager->add($post1); + $post2 = new Tag('hallo 2'); + $this->persistenceManager->add($post2); $this->persistenceManager->persistAll(); $response = $this->browser->request('http://localhost/test/fluidrenderfunctions/test/select'); From 9f3488f84e23f80b47ef3ce1d437548259902aad Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sat, 1 Jun 2024 23:33:12 +0200 Subject: [PATCH 4/8] try to create forward compatibility with Flow 7.x forwards --- Classes/Utils/DummyView.php | 1 + Classes/Utils/NodeRenderTransfer.php | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Classes/Utils/DummyView.php b/Classes/Utils/DummyView.php index ca39d9b..896efa1 100644 --- a/Classes/Utils/DummyView.php +++ b/Classes/Utils/DummyView.php @@ -6,6 +6,7 @@ namespace DigiComp\FluidRenderFunctions\Utils; use TYPO3Fluid\Fluid\View\ViewInterface; +// @deprecated: drop this class, as soon as Flow 6.3 support drops class DummyView implements ViewInterface { public function assign($key, $value) diff --git a/Classes/Utils/NodeRenderTransfer.php b/Classes/Utils/NodeRenderTransfer.php index 068d605..213c0be 100644 --- a/Classes/Utils/NodeRenderTransfer.php +++ b/Classes/Utils/NodeRenderTransfer.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace DigiComp\FluidRenderFunctions\Utils; use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface; +use Neos\Flow\ObjectManagement\Proxy\ProxyInterface; use Neos\FluidAdaptor\Core\Rendering\RenderingContext; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface; @@ -28,7 +29,22 @@ class NodeRenderTransfer implements InvokeRenderFunctionInterface public function __invoke($object): string { - $context = new RenderingContext(new DummyView()); + // @deprecated: drop the condition, if compatibility to Flow 6.3 can be dropped + $reflector = new \ReflectionClass(RenderingContext::class); + if ($reflector->implementsInterface(ProxyInterface::class)) { + try { + $reflector = new \ReflectionClass($reflector->getParentClass()->getName()); + } catch (\Exception $e) { + // nothing, go with the first one + } + } + $constructor = $reflector->getConstructor(); + + if ($constructor->getNumberOfRequiredParameters() > 0) { + $context = new RenderingContext(new DummyView()); + } else { + $context = new RenderingContext(); + } $context->getVariableProvider()->add($this->subjectName, $object); return \trim($this->node->evaluate($context)); } From 050e6e119fd4130c6b58462b46c8503cf70153ad Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sun, 2 Jun 2024 04:04:48 +0200 Subject: [PATCH 5/8] storing the render function in ViewHelperVariableContainer --- .../ApplyRenderFunctionViewHelper.php | 11 +++++++- .../RegisterRenderFunctionViewHelper.php | 5 ++-- .../Traits/ValidateRenderFunctionTrait.php | 26 +++++++++++++++++++ README.md | 2 +- .../Tests/Functional/Fixtures/Test/Index.html | 2 +- .../Functional/Fixtures/Test/Select.html | 2 +- 6 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 Classes/ViewHelpers/Traits/ValidateRenderFunctionTrait.php diff --git a/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php b/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php index cf97859..02539a1 100644 --- a/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php +++ b/Classes/ViewHelpers/ApplyRenderFunctionViewHelper.php @@ -7,15 +7,18 @@ namespace DigiComp\FluidRenderFunctions\ViewHelpers; use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface; use DigiComp\FluidRenderFunctions\Utils\GeneratorClosureIterator; use DigiComp\FluidRenderFunctions\Utils\RenderableProxy; +use DigiComp\FluidRenderFunctions\ViewHelpers\Traits\ValidateRenderFunctionTrait; use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper; class ApplyRenderFunctionViewHelper extends AbstractViewHelper { + use ValidateRenderFunctionTrait; + public function initializeArguments(): void { parent::initializeArguments(); $this->registerArgument('in', 'mixed', 'subject to apply the render function to'); - $this->registerArgument('function', InvokeRenderFunctionInterface::class, 'render function to use', true); + $this->registerArgument('function', 'string', 'render function to use', true); $this->registerArgument( 'force', 'bool', @@ -26,6 +29,12 @@ class ApplyRenderFunctionViewHelper extends AbstractViewHelper ); } + public function validateArguments() + { + parent::validateArguments(); + $this->validateRenderFunctionArgument('function'); + } + public function render() { $in = $this->arguments['in']; diff --git a/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php b/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php index 5db7772..511cae7 100644 --- a/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php +++ b/Classes/ViewHelpers/RegisterRenderFunctionViewHelper.php @@ -47,8 +47,9 @@ class RegisterRenderFunctionViewHelper extends AbstractViewHelper foreach ($this->childNodes as $childNode) { $transferNode->addChildNode($childNode); } - $this->renderingContext->getVariableProvider() - ->add($this->arguments['as'], new NodeRenderTransfer($transferNode, $this->arguments['subjectName'])); + $renderer = new NodeRenderTransfer($transferNode, $this->arguments['subjectName']); + $this->renderingContext->getViewHelperVariableContainer() + ->add(static::class, $this->arguments['as'], $renderer); return ''; } } diff --git a/Classes/ViewHelpers/Traits/ValidateRenderFunctionTrait.php b/Classes/ViewHelpers/Traits/ValidateRenderFunctionTrait.php new file mode 100644 index 0000000..c486f4d --- /dev/null +++ b/Classes/ViewHelpers/Traits/ValidateRenderFunctionTrait.php @@ -0,0 +1,26 @@ +viewHelperVariableContainer->get( + RegisterRenderFunctionViewHelper::class, + $this->arguments[$argumentName] + ); + if (!($renderFunction instanceof InvokeRenderFunctionInterface)) { + throw new \InvalidArgumentException( + 'render function with name "' . $this->arguments[$argumentName] . '" has not been registered.', + 1717293038 + ); + } + $this->arguments[$argumentName] = $renderFunction; + } +} diff --git a/README.md b/README.md index 9dc2ced..f5f553a 100644 --- a/README.md +++ b/README.md @@ -13,5 +13,5 @@ FluidRenderFunctions to the rescue: {myBook.name} from {myBook.author.name} - + ``` diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html index 369d8a4..ab764ae 100644 --- a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html @@ -4,5 +4,5 @@ {subject.name} is cool - {test -> rf:applyRenderFunction(function: testFunc, force: true)} + {test -> rf:applyRenderFunction(function: 'testFunc', force: true)} diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html index 1899cee..120ca49 100644 --- a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Select.html @@ -4,5 +4,5 @@ {subject.name} is cool - + From 098d5c09a62978d9f4ee2f312e2edcefba4416a9 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sun, 2 Jun 2024 15:36:25 +0200 Subject: [PATCH 6/8] adding augmentations for select and textfield ViewHelpers --- Classes/FormExtensions/SelectAspect.php | 69 +++++++++++++++++++ Classes/FormExtensions/TextfieldAspect.php | 59 ++++++++++++++++ Classes/Utils/RenderableProxy.php | 2 +- Configuration/Settings.yaml | 5 ++ README.md | 8 +++ .../Fixtures/Test/ExtendedSelect.html | 6 ++ .../Fixtures/Test/ExtendedTextfield.html | 11 +++ .../Fixtures/Controller/TestController.php | 12 ++++ Tests/Functional/Fixtures/Domain/Post.php | 43 ++++++++++++ Tests/Functional/RenderFunctionsTest.php | 46 ++++++++++++- 10 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 Classes/FormExtensions/SelectAspect.php create mode 100644 Classes/FormExtensions/TextfieldAspect.php create mode 100644 Configuration/Settings.yaml create mode 100644 Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedSelect.html create mode 100644 Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedTextfield.html create mode 100644 Tests/Functional/Fixtures/Domain/Post.php diff --git a/Classes/FormExtensions/SelectAspect.php b/Classes/FormExtensions/SelectAspect.php new file mode 100644 index 0000000..43b87e6 --- /dev/null +++ b/Classes/FormExtensions/SelectAspect.php @@ -0,0 +1,69 @@ +initializeArguments())") + */ + public function introduceRenderFuncArgument(JoinPointInterface $joinPoint): void + { + $proxy = $joinPoint->getProxy(); + if (!($proxy instanceof SelectViewHelper)) { + return; + } + $proxy->registerArgument('renderFunction', 'string', 'callabe to use to render single object'); + } + + /** + * @Flow\After("setting(DigiComp.FluidRenderFunctions.enableAspects.select) && method(Neos\FluidAdaptor\ViewHelpers\Form\SelectViewHelper->validateArguments())") + */ + public function validateRenderFunction(JoinPointInterface $joinPoint): void + { + $proxy = $joinPoint->getProxy(); + if (!($proxy instanceof SelectViewHelper)) { + return; + } + if (!isset($proxy->arguments['renderFunction'])) { + return; + } + $renderFunction = $proxy->viewHelperVariableContainer->get( + RegisterRenderFunctionViewHelper::class, + $proxy->arguments['renderFunction'] + ); + if (!($renderFunction instanceof InvokeRenderFunctionInterface)) { + throw new \InvalidArgumentException( + 'render function with name "' . $proxy->arguments['renderFunction'] . '" has not been registered.', + 1717293038 + ); + } + + $proxy->arguments['renderFunction'] = $renderFunction; + $originalOptions = $proxy->arguments['options']; + if (!\is_iterable($originalOptions)) { + // Validation is left to the original view helper + return; + } + $proxy->arguments['options'] = new GeneratorClosureIterator( + static function () use ($originalOptions, $renderFunction) { + foreach ($originalOptions as $option) { + yield new RenderableProxy($renderFunction, $option); + } + } + ); + } +} diff --git a/Classes/FormExtensions/TextfieldAspect.php b/Classes/FormExtensions/TextfieldAspect.php new file mode 100644 index 0000000..a116e64 --- /dev/null +++ b/Classes/FormExtensions/TextfieldAspect.php @@ -0,0 +1,59 @@ +initializeArguments())") + */ + public function introduceRenderFuncArgument(JoinPointInterface $joinPoint): void + { + $proxy = $joinPoint->getProxy(); + if (!($proxy instanceof TextfieldViewHelper)) { + return; + } + $proxy->registerArgument('renderFunction', 'string', 'callabe to use to render single object'); + } + + /** + * @Flow\Around("setting(DigiComp.FluidRenderFunctions.enableAspects.textfield) && method(Neos\FluidAdaptor\ViewHelpers\Form\TextfieldViewHelper->getValueAttribute())") + */ + public function applyRenderFunctionToValue(JoinPointInterface $joinPoint) + { + $proxy = $joinPoint->getProxy(); + if (!($proxy instanceof TextfieldViewHelper)) { + return; + } + if (!isset($proxy->arguments['renderFunction'])) { + return $joinPoint->getAdviceChain()->proceed($joinPoint); + } + $renderFunction = $proxy->viewHelperVariableContainer->get( + RegisterRenderFunctionViewHelper::class, + $proxy->arguments['renderFunction'] + ); + if (!($renderFunction instanceof InvokeRenderFunctionInterface)) { + throw new \InvalidArgumentException( + 'render function with name "' . $proxy->arguments['renderFunction'] . '" has not been registered.', + 1717293038 + ); + } + $originalObject = $joinPoint->getAdviceChain()->proceed($joinPoint); + if (\is_object($originalObject)) { + return new RenderableProxy($renderFunction, $originalObject); + } + return $originalObject; + } +} diff --git a/Classes/Utils/RenderableProxy.php b/Classes/Utils/RenderableProxy.php index 2ab78de..1540dcc 100644 --- a/Classes/Utils/RenderableProxy.php +++ b/Classes/Utils/RenderableProxy.php @@ -17,7 +17,7 @@ class RenderableProxy { protected InvokeRenderFunctionInterface $renderFunction; protected $object; - public string $Persistence_Object_Identifier = ''; + public ?string $Persistence_Object_Identifier = null; public function __construct(InvokeRenderFunctionInterface $renderFunction, $object) { diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml new file mode 100644 index 0000000..763eab0 --- /dev/null +++ b/Configuration/Settings.yaml @@ -0,0 +1,5 @@ +DigiComp: + FluidRenderFunctions: + enableAspects: + select: true + textfield: true diff --git a/README.md b/README.md index f5f553a..5cdbaf6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DigiComp.FluidRenderFunctions +## Quickstart + This Package provides you with the possibility to register render functions, created from them, to use them dynamically else where. Let me show you the idea: @@ -15,3 +17,9 @@ FluidRenderFunctions to the rescue: ``` + +To make your live easier, FluidRenderFunctions augments the original `SelectViewHelper` and the `TextfieldViewHelper` with an optional `renderFunction` argument. That way, you can even use the usual Textfield to display formatted Datetime objects. Neat! + +## Configuration + +If - for whatever reason - you do not want FluidRenderFunctions to augment the original ViewHelpers you can opt out by setting `DigiComp.FluidRenderFunctions.enableAspects.select` or `DigiComp.FluidRenderFunctions.enableAspects.textfield` to `false`. diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedSelect.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedSelect.html new file mode 100644 index 0000000..185c717 --- /dev/null +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedSelect.html @@ -0,0 +1,6 @@ +{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers} + + + {subject.name} is cool + + diff --git a/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedTextfield.html b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedTextfield.html new file mode 100644 index 0000000..548c86e --- /dev/null +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/ExtendedTextfield.html @@ -0,0 +1,11 @@ +{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers} + + + {subject -> f:format.date(format: 'd.m.Y')} + + + + + + + diff --git a/Tests/Functional/Fixtures/Controller/TestController.php b/Tests/Functional/Fixtures/Controller/TestController.php index 64f75b0..16238ae 100644 --- a/Tests/Functional/Fixtures/Controller/TestController.php +++ b/Tests/Functional/Fixtures/Controller/TestController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace DigiComp\FluidRenderFunctions\Tests\Functional\Fixtures\Controller; +use DigiComp\FluidRenderFunctions\Tests\Functional\Fixtures\Domain\Post; use Neos\Flow\Mvc\Controller\ActionController; use Neos\Flow\Persistence\Doctrine\Query; use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag; @@ -19,4 +20,15 @@ class TestController extends ActionController { $this->view->assign('testEntities', (new Query(Tag::class))->execute()); } + + public function extendedSelectAction() + { + $this->view->assign('tags', (new Query(Tag::class))->execute()); + } + + public function extendedTextfieldAction() + { + $this->view->assign('now', new \DateTimeImmutable('2024-06-02T15:03:00Z')); + $this->view->assign('post', (new Query(Post::class))->execute()->getFirst()); + } } diff --git a/Tests/Functional/Fixtures/Domain/Post.php b/Tests/Functional/Fixtures/Domain/Post.php new file mode 100644 index 0000000..6d18f51 --- /dev/null +++ b/Tests/Functional/Fixtures/Domain/Post.php @@ -0,0 +1,43 @@ +title = $title; + $this->publishedAt = $publishedAt; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function getPublishedAt(): ?\DateTimeImmutable + { + return $this->publishedAt; + } +} diff --git a/Tests/Functional/RenderFunctionsTest.php b/Tests/Functional/RenderFunctionsTest.php index 6452834..65f633c 100644 --- a/Tests/Functional/RenderFunctionsTest.php +++ b/Tests/Functional/RenderFunctionsTest.php @@ -4,9 +4,11 @@ declare(strict_types=1); namespace DigiComp\FluidRenderFunctions\Tests\Functional; +use DigiComp\FluidRenderFunctions\Tests\Functional\Fixtures\Domain\Post; use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag; +use Symfony\Component\DomCrawler\Crawler; class RenderFunctionsTest extends FunctionalTestCase { @@ -51,8 +53,46 @@ class RenderFunctionsTest extends FunctionalTestCase $this->persistenceManager->add($post2); $this->persistenceManager->persistAll(); - $response = $this->browser->request('http://localhost/test/fluidrenderfunctions/test/select'); - static::assertStringContainsString('hallo is cool', (string)$response->getBody()); - static::assertStringContainsString('hallo 2 is cool', (string)$response->getBody()); + $this->browser->request('http://localhost/test/fluidrenderfunctions/test/select'); + $options = $this->browser->getCrawler() + ->filterXPath('//select[1]/option') + ->each(fn (Crawler $node) => $node->text()); + static::assertEquals(['hallo is cool', 'hallo 2 is cool'], $options); + } + + /** + * @test + */ + public function itAllowsRenderFunctionOnStandardSelect(): void + { + $post1 = new Tag('hallo'); + $this->persistenceManager->add($post1); + $post2 = new Tag('hallo 2'); + $this->persistenceManager->add($post2); + $this->persistenceManager->persistAll(); + + $this->browser->request('http://localhost/test/fluidrenderfunctions/test/extendedselect'); + $options = $this->browser->getCrawler() + ->filterXPath('//select[1]/option') + ->each(fn (Crawler $node) => $node->text()); + static::assertEquals(['hallo is cool', 'hallo 2 is cool'], $options); + } + + /** + * @test + */ + public function itAllowsRenderFunctionOnStandardTextField(): void + { + $testDate = new \DateTimeImmutable('2024-06-02T15:03:00Z'); + + $post1 = new Post('hallo', $testDate); + $this->persistenceManager->add($post1); + $this->persistenceManager->persistAll(); + + $this->browser->request('http://localhost/test/fluidrenderfunctions/test/extendedtextfield'); + $input1 = $this->browser->getCrawler()->filterXPath('//input[@type="text"][1]/@value')->text('input not found'); + static::assertEquals('02.06.2024', $input1); + $input2 = $this->browser->getCrawler()->filterXPath('//input[@type="text"][2]/@value')->text('input not found'); + static::assertEquals('02.06.2024', $input2); } } From dae60670a40a7038d8a341180af953d8b7aa988f Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Tue, 4 Jun 2024 21:57:05 +0200 Subject: [PATCH 7/8] updating ci pipelines with own images --- .woodpecker/code-style.yml | 9 +++------ .woodpecker/functional-tests.yml | 18 +++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/.woodpecker/code-style.yml b/.woodpecker/code-style.yml index 6f13ed1..0eb985f 100644 --- a/.woodpecker/code-style.yml +++ b/.woodpecker/code-style.yml @@ -1,10 +1,7 @@ steps: code-style: - image: composer - commands: - - composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer - - composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - - composer global require digicomp/php-codesniffer:@dev - - composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/ + image: git.digital-competence.de/woodpecker-ci/plugin-phpcs + settings: + args: Classes/ Tests/ when: - event: [push, pull_request, manual] diff --git a/.woodpecker/functional-tests.yml b/.woodpecker/functional-tests.yml index 2dbb2a3..e81fadc 100644 --- a/.woodpecker/functional-tests.yml +++ b/.woodpecker/functional-tests.yml @@ -15,20 +15,8 @@ matrix: steps: functional-tests: - image: "thecodingmachine/php:${PHP_VERSION}-v4-cli" - environment: - # Enable the PDO_SQLITE extension - - "PHP_EXTENSION_PDO_SQLITE=1" - - "FLOW_VERSION=${FLOW_VERSION}" - - "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}" - commands: - - "sudo mkdir $NEOS_BUILD_DIR" - - "sudo chown -R docker:docker $NEOS_BUILD_DIR" - - "cd $NEOS_BUILD_DIR" - - "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ." - - "composer config repositories.repo-name path /woodpecker/package" - - "composer remove --dev --no-update neos/behat || composer remove --no-update neos/behat" - - "composer require digicomp/fluid-render-functions:@dev" - - "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FluidRenderFunctions/Tests/Functional" + image: git.digital-competence.de/woodpecker-ci/plugin-phpunit-flow:${PHP_VERSION} + settings: + flow_version: ${FLOW_VERSION} when: - event: [ push, pull_request, manual ] From ed7d6ae002e02a02a42c30f84617fcabd0adf8b0 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Tue, 4 Jun 2024 23:41:59 +0200 Subject: [PATCH 8/8] License.txt => LICENSE --- License.txt => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename License.txt => LICENSE (100%) diff --git a/License.txt b/LICENSE similarity index 100% rename from License.txt rename to LICENSE