First working version
Some checks failed
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests/1 Pipeline failed
ci/woodpecker/push/functional-tests/2 Pipeline failed
ci/woodpecker/push/functional-tests/3 Pipeline failed
ci/woodpecker/push/functional-tests/4 Pipeline failed

This commit is contained in:
Ferdinand Kuhl 2024-06-01 23:15:35 +02:00
parent 7bdd5494d6
commit 0de901d45c
17 changed files with 460 additions and 0 deletions

View file

@ -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]

View file

@ -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 ]

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions;
interface InvokeRenderFunctionInterface
{
public function __invoke($object): string;
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions\Utils;
use TYPO3Fluid\Fluid\View\ViewInterface;
class DummyView implements ViewInterface
{
public function assign($key, $value)
{
}
public function assignMultiple(array $values)
{
}
public function render()
{
}
public function renderSection($sectionName, array $variables = [], $ignoreUnknown = false)
{
}
public function renderPartial($partialName, $sectionName, array $variables, $ignoreUnknown = false)
{
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace DigiComp\FluidRenderFunctions\Utils;
class GeneratorClosureIterator implements \IteratorAggregate
{
private \Closure $closure;
/**
* @param \Closure $closure
*/
public function __construct(\Closure $closure)
{
$this->closure = $closure;
}
public function getIterator(): \Generator
{
return ($this->closure)();
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions\Utils;
use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface;
use Neos\FluidAdaptor\Core\Rendering\RenderingContext;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
class NodeRenderTransfer implements InvokeRenderFunctionInterface
{
/**
* @var NodeInterface
*/
protected NodeInterface $node;
/**
* @var string
*/
protected string $subjectName;
public function __construct(NodeInterface $node, string $subjectName = 'object')
{
$this->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));
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions\Utils;
use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface;
use Neos\Utility\Exception\PropertyNotAccessibleException;
use Neos\Utility\ObjectAccess;
/**
* The RenderableProxy wraps original objects and provides a __toString() function
*
* Because of that, you could use it, to wrap your objects in it and pass the wrapped objects to SelectViewHelper
*/
class RenderableProxy
{
protected InvokeRenderFunctionInterface $renderFunction;
protected $object;
public string $Persistence_Object_Identifier = '';
public function __construct(InvokeRenderFunctionInterface $renderFunction, $object)
{
$this->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);
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions\ViewHelpers;
use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface;
use DigiComp\FluidRenderFunctions\Utils\GeneratorClosureIterator;
use DigiComp\FluidRenderFunctions\Utils\RenderableProxy;
use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper;
class ApplyRenderFunctionViewHelper extends AbstractViewHelper
{
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(
'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);
}
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidRenderFunctions\ViewHelpers;
use DigiComp\FluidRenderFunctions\Utils\NodeRenderTransfer;
use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
class RegisterRenderFunctionViewHelper extends AbstractViewHelper
{
/**
* @throws \Neos\FluidAdaptor\Core\ViewHelper\Exception
*/
public function initializeArguments(): void
{
parent::initializeArguments();
$this->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 '';
}
}

19
License.txt Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Ferdinand Kuhl <f.kuhl@digital-competence.de>
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.

17
README.md Normal file
View file

@ -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
<f:form.select options="{books}" />
```
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
<rf:registerRenderFunction as="renderBook" subjectName="myBook">
{myBook.name} from {myBook.author.name}
</rf:registerRenderFunction>
<f:form.select options="{books -> rf:applyRenderFunction(function: renderBook)}" />
```

View file

@ -0,0 +1 @@
<f:render section="Content" />

View file

@ -0,0 +1,8 @@
<f:layout name="Test" />
{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers}
<f:section name="Content"><f:spaceless>
<rf:registerRenderFunction as="testFunc">
{subject.name} is cool
</rf:registerRenderFunction>
{test -> rf:applyRenderFunction(function: testFunc, force: true)}
</f:spaceless></f:section>

View file

@ -0,0 +1,8 @@
<f:layout name="Test" />
{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers}
<f:section name="Content"><f:spaceless>
<rf:registerRenderFunction as="testFunc">
{subject.name} is cool
</rf:registerRenderFunction>
<f:form.select options="{testEntities -> rf:applyRenderFunction(function: testFunc)}" />
</f:spaceless></f:section>

View file

@ -0,0 +1,22 @@
<?php
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;
class TestController extends ActionController
{
public function indexAction()
{
$this->view->assign('test', ['name' => 'hallo']);
}
public function selectAction()
{
$this->view->assign('testEntities', (new Query(TestEntity::class))->execute());
}
}

View file

@ -0,0 +1,59 @@
<?php
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;
class RenderFunctionsTest extends FunctionalTestCase
{
protected static $testablePersistenceEnabled = true;
/**
* Initializer
*/
protected function setUp(): void
{
parent::setUp();
$route = new Route();
$route->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());
}
}

45
composer.json Normal file
View file

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