Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
0f7f872522 | |||
a68732c45f | |||
baf5636bd3 | |||
7585baee36 |
12 changed files with 460 additions and 0 deletions
7
.woodpecker/code-style.yml
Normal file
7
.woodpecker/code-style.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
steps:
|
||||
code-style:
|
||||
image: git.digital-competence.de/woodpecker-ci/plugin-phpcs
|
||||
settings:
|
||||
args: Classes/ Tests/
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
30
.woodpecker/functional-tests.yml
Normal file
30
.woodpecker/functional-tests.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
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: git.digital-competence.de/woodpecker-ci/plugin-phpunit-flow:${PHP_VERSION}
|
||||
settings:
|
||||
flow_version: ${FLOW_VERSION}
|
||||
ssh_key:
|
||||
from_secret: deploykey
|
||||
hostkeys:
|
||||
- digital-competence.de ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNSjVKJ+SO6wqmDSCgcJDk2ljWlD7qajsTxAuvZpTbJBg2++Zu0VxH0S1WzPVTD/D5UUbK6LVy6YSCnGlv6zmc0=
|
||||
repositories:
|
||||
- vcs ssh://git@digital-competence.de/Packages/DigiComp.FluidRenderFunctions
|
||||
require:
|
||||
- digicomp/fluid-render-functions:@dev
|
||||
when:
|
||||
- event: [ push, pull_request, manual ]
|
143
Classes/ViewHelpers/Controller/FluidJsonController.php
Normal file
143
Classes/ViewHelpers/Controller/FluidJsonController.php
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DigiComp\FluidJsonViews\ViewHelpers\Controller;
|
||||
|
||||
use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Proxy\Proxy;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Neos\Flow\Annotations as Flow;
|
||||
use Neos\Flow\Persistence\Doctrine\Query;
|
||||
use Neos\FluidAdaptor\Core\Widget\AbstractWidgetController;
|
||||
use Neos\FluidAdaptor\View\TemplateView;
|
||||
use Neos\Utility\TypeHandling;
|
||||
|
||||
class FluidJsonController extends AbstractWidgetController
|
||||
{
|
||||
/**
|
||||
* @Flow\Inject
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $supportedMediaTypes = [
|
||||
'text/html',
|
||||
'application/json',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Query|null
|
||||
*/
|
||||
protected ?Query $query = null;
|
||||
|
||||
protected InvokeRenderFunctionInterface $renderFunction;
|
||||
|
||||
/**
|
||||
* @Flow\InjectConfiguration(package="DigiComp.FluidJsonViews", path="limitConfiguration")
|
||||
* @var array
|
||||
*/
|
||||
protected array $limitConfiguration;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $searchProperties;
|
||||
|
||||
protected function initializeAction(): void
|
||||
{
|
||||
parent::initializeAction();
|
||||
|
||||
$this->query = clone $this->widgetConfiguration['objects']->getQuery();
|
||||
$this->renderFunction = $this->widgetConfiguration['renderFunction'];
|
||||
$this->searchProperties = $this->widgetConfiguration['searchProperties'];
|
||||
$this->initializeDoctrineSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes count, objectType and doctrine's MetaDataFactory
|
||||
*/
|
||||
protected function initializeDoctrineSource(): void
|
||||
{
|
||||
// As we are working with objects which may be doctrine proxies persisted in session, its metadata have
|
||||
// never been loaded in this request. So let's see these parameters and load their metadata, so the doctrine
|
||||
// framework knows how to go on.
|
||||
if ($this->query instanceof Query) {
|
||||
$parameters = $this->query->getQueryBuilder()->getParameters();
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter instanceof Parameter && $parameter->getValue() instanceof Proxy) {
|
||||
$this->entityManager->getMetadataFactory()->getMetadataFor(
|
||||
TypeHandling::getTypeForValue($parameter->getValue())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->assign('entityClassName', $this->query->getType());
|
||||
if ($this->view instanceof TemplateView) {
|
||||
$renderingContext = $this->request
|
||||
->getInternalArgument('__widgetContext')
|
||||
->getViewHelperChildNodeRenderingContext();
|
||||
$this->view->assignMultiple($renderingContext->getVariableProvider()->getAll());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://select2.org/data-sources/ajax#request-parameters
|
||||
*/
|
||||
public function dataAction(
|
||||
?int $limit = null,
|
||||
?string $term = null,
|
||||
?int $page = 0
|
||||
) {
|
||||
$query = $this->query;
|
||||
$result['recordsTotal'] = $query->execute()->count();
|
||||
if ($term !== null) {
|
||||
$searchConstraint = $this->getSearchConstraints($term);
|
||||
$query->matching($query->logicalAnd($searchConstraint));
|
||||
}
|
||||
|
||||
$result['recordsFiltered'] = $query->execute()->count();
|
||||
if ($limit === null) {
|
||||
$limit = $this->limitConfiguration['default'];
|
||||
}
|
||||
if ($this->limitConfiguration['max'] !== null) {
|
||||
if ($limit > $this->limitConfiguration['max'] || $limit === null) {
|
||||
$limit = $this->limitConfiguration['max'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$data = $query->setLimit($limit)->setOffset($page * $limit)->execute();
|
||||
} else {
|
||||
$data = $query->execute();
|
||||
}
|
||||
|
||||
$renderFunc = $this->renderFunction;
|
||||
foreach ($data as $object) {
|
||||
$result['results'][] = [
|
||||
'id' => $this->persistenceManager->getIdentifierByObject($object),
|
||||
'text' => $renderFunc($object),
|
||||
];
|
||||
}
|
||||
$this->response->setContentType('application/json');
|
||||
return \json_encode($result, \JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
protected function getSearchConstraints(
|
||||
string $term
|
||||
): object {
|
||||
$searchConstraints = [];
|
||||
foreach ($this->searchProperties as $searchProperty) {
|
||||
$searchConstraints[] = $this->query->like($searchProperty, '%' . $term . '%');
|
||||
}
|
||||
return $this->query->logicalOr($searchConstraints);
|
||||
}
|
||||
}
|
72
Classes/ViewHelpers/FluidJsonViewHelper.php
Normal file
72
Classes/ViewHelpers/FluidJsonViewHelper.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DigiComp\FluidJsonViews\ViewHelpers;
|
||||
|
||||
use DigiComp\FluidJsonViews\ViewHelpers\Controller\FluidJsonController;
|
||||
use DigiComp\FluidRenderFunctions\ViewHelpers\Traits\ValidateRenderFunctionTrait;
|
||||
use Neos\Flow\Annotations as Flow;
|
||||
use Neos\Flow\Mvc\Exception\InfiniteLoopException;
|
||||
use Neos\Flow\Mvc\Exception\StopActionException;
|
||||
use Neos\Flow\Persistence\Doctrine\QueryResult;
|
||||
use Neos\FluidAdaptor\Core\Widget\AbstractWidgetViewHelper;
|
||||
use Neos\FluidAdaptor\Core\Widget\Exception\InvalidControllerException;
|
||||
use Neos\FluidAdaptor\Core\Widget\Exception\MissingControllerException;
|
||||
|
||||
class FluidJsonViewHelper extends AbstractWidgetViewHelper
|
||||
{
|
||||
use ValidateRenderFunctionTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @Flow\Inject
|
||||
* @var FluidJsonController
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $ajaxWidget = true;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function initializeArguments(): void
|
||||
{
|
||||
parent::initializeArguments();
|
||||
|
||||
$this->registerArgument('objects', QueryResult::class, 'Objects to show in table.', true);
|
||||
$this->registerArgument(
|
||||
'renderFunction',
|
||||
'string',
|
||||
'callabe to use to render single object',
|
||||
true
|
||||
);
|
||||
$this->registerArgument(
|
||||
'searchProperties',
|
||||
'array',
|
||||
'an array of pathes, which should be used during search evaluation',
|
||||
false,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
public function validateArguments()
|
||||
{
|
||||
parent::validateArguments();
|
||||
$this->validateRenderFunctionArgument('renderFunction');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InfiniteLoopException
|
||||
* @throws InvalidControllerException
|
||||
* @throws MissingControllerException
|
||||
* @throws StopActionException
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
return $this->initiateSubRequest();
|
||||
}
|
||||
}
|
5
Configuration/Settings.yaml
Normal file
5
Configuration/Settings.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
DigiComp:
|
||||
FluidJsonViews:
|
||||
limitConfiguration:
|
||||
default: 1000
|
||||
max: 1000
|
19
LICENSE
Normal file
19
LICENSE
Normal 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.
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# DigiComp.FluidJsonViews
|
||||
|
||||
This package builds upon `DigiComp.FluidRenderFunctions` and uses this, to use a such defined render function to create a simple key/value json view from your QueryResult. Where the key will be the persistence identifier and the value the result of your rendered template.
|
||||
|
||||
Let me provide you an example:
|
||||
```html
|
||||
<rf:registerRenderFunction as="renderTag">
|
||||
{subject.name}
|
||||
</rf:registerRenderFunction>
|
||||
<fj:fluidJson objects="{tags}" renderFunction="renderTag" searchProperties="{0: 'name'}">
|
||||
<a href="{dataUri}">jsonView</a>
|
||||
</fj:fluidJson>
|
||||
```
|
||||
|
||||
If you fetch the jsonView you will see something like this:
|
||||
```json
|
||||
{
|
||||
"recordsTotal": 2,
|
||||
"recordsFiltered": 2,
|
||||
"results": [
|
||||
{"id": "a310057f-869e-419e-b6fe-6c3a00fe444a", "text": "hallo 2"},
|
||||
{"id": "a310057f-869e-419e-b6fe-6c3a00fe444b", "text": "hallo"}
|
||||
]
|
||||
}
|
||||
```
|
||||
The provided link will understand three query parameters: `limit`, `term` and `page` - all optional.
|
||||
If you send a "term" parameter, it will apply an or sql search over all your given search property paths. `limit` and `page` will allow you to paginate the results.
|
||||
For security and performance reasons, you can define a max limit using the provided values in Settings.yaml. There you can change the default limit (used, if not sent), or disable both (not recommended).
|
|
@ -0,0 +1,8 @@
|
|||
{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers}
|
||||
{namespace fj=DigiComp\FluidJsonViews\ViewHelpers}
|
||||
<rf:registerRenderFunction as="renderTag">
|
||||
{subject.name}
|
||||
</rf:registerRenderFunction>
|
||||
<fj:fluidJson objects="{tags}" renderFunction="renderTag" searchProperties="{0: 'name'}">
|
||||
<a href="{dataUri}">jsonView</a>
|
||||
</fj:fluidJson>
|
|
@ -0,0 +1,4 @@
|
|||
<f:renderChildren arguments="{
|
||||
dataUri: '{f:widget.uri(action: \'data\', format: \'json\', ajax: true)}',
|
||||
entityClassName: entityClassName
|
||||
}" />
|
17
Tests/Functional/Fixtures/Controller/TestController.php
Normal file
17
Tests/Functional/Fixtures/Controller/TestController.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DigiComp\FluidJsonViews\Tests\Functional\Fixtures\Controller;
|
||||
|
||||
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
|
||||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->assign('tags', (new Query(Tag::class))->execute());
|
||||
}
|
||||
}
|
78
Tests/Functional/FluidJsonTest.php
Normal file
78
Tests/Functional/FluidJsonTest.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DigiComp\FluidJsonViews\Tests\Functional;
|
||||
|
||||
use Neos\Flow\Mvc\Routing\Route;
|
||||
use Neos\Flow\Tests\FunctionalTestCase;
|
||||
use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag;
|
||||
|
||||
class FluidJsonTest extends FunctionalTestCase
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected static $testablePersistenceEnabled = true;
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$route = new Route();
|
||||
$route->setUriPattern('test/fluidjsonviews/test(/{@action})');
|
||||
$route->setDefaults([
|
||||
'@package' => 'DigiComp.FluidJsonViews',
|
||||
'@subpackage' => 'Tests\Functional\Fixtures',
|
||||
'@controller' => 'Test',
|
||||
'@action' => 'index',
|
||||
]);
|
||||
$route->setAppendExceedingArguments(true);
|
||||
$this->router->addRoute($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function itAllowsFluidToRenderWrappedArray(): 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/fluidjsonviews/test');
|
||||
$link = $this->browser->getCrawler()->selectLink('jsonView')->link()->getUri();
|
||||
$response = $this->browser->request($link);
|
||||
static::assertEquals('application/json', $response->getHeaderLine('Content-Type'));
|
||||
$result = \json_decode((string)$response->getBody(), true);
|
||||
static::assertEquals(2, $result['recordsTotal']);
|
||||
static::assertCount(2, $result['results']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function itFiltersIfTermProvided(): 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/fluidjsonviews/test');
|
||||
$link = $this->browser->getCrawler()->selectLink('jsonView')->link()->getUri();
|
||||
$response = $this->browser->request($link . '&term=2');
|
||||
$result = \json_decode((string)$response->getBody(), true);
|
||||
static::assertEquals('application/json', $response->getHeaderLine('Content-Type'));
|
||||
|
||||
static::assertEquals(2, $result['recordsTotal']);
|
||||
static::assertEquals(1, $result['recordsFiltered']);
|
||||
static::assertCount(1, $result['results']);
|
||||
}
|
||||
}
|
49
composer.json
Normal file
49
composer.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "digicomp/fluid-json-views",
|
||||
"description": "create simple json views, with remote search from Fluid",
|
||||
"type": "neos-package",
|
||||
"require": {
|
||||
"digicomp/fluid-render-functions": "^1.0.0",
|
||||
"ext-json": "*",
|
||||
"neos/flow": "^6.3.5 | ^7.3 | ^8.3",
|
||||
"neos/fluid-adaptor": "^6.3.5 | ^7.3 | ^8.3",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~8.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DigiComp\\FluidJsonViews\\": "Classes/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"DigiComp\\FluidJsonViews\\Tests\\": "Tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"neos": {
|
||||
"package-key": "DigiComp.FluidJsonViews"
|
||||
},
|
||||
"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.FluidJsonViews",
|
||||
"keywords": [
|
||||
"Neos",
|
||||
"Flow",
|
||||
"fluid",
|
||||
"ajax"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue