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" + ] +}