From 7585baee3648a138b6dd65cda06dfeec6763e9db Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Sun, 2 Jun 2024 01:59:15 +0200 Subject: [PATCH] first working version in Flow 6.3 --- .woodpecker/code-style.yml | 10 ++ .woodpecker/functional-tests.yml | 42 ++++++ .../Controller/FluidJsonController.php | 136 ++++++++++++++++++ Classes/ViewHelpers/FluidJsonViewHelper.php | 64 +++++++++ Configuration/Settings.yaml | 5 + License.txt | 19 +++ README.md | 28 ++++ .../Tests/Functional/Fixtures/Test/Index.html | 8 ++ .../ViewHelpers/FluidJson/Index.html | 4 + .../Fixtures/Controller/TestController.php | 17 +++ Tests/Functional/FluidJsonTest.php | 78 ++++++++++ composer.json | 49 +++++++ 12 files changed, 460 insertions(+) create mode 100644 .woodpecker/code-style.yml create mode 100644 .woodpecker/functional-tests.yml create mode 100644 Classes/ViewHelpers/Controller/FluidJsonController.php create mode 100644 Classes/ViewHelpers/FluidJsonViewHelper.php create mode 100644 Configuration/Settings.yaml create mode 100644 License.txt create mode 100644 README.md create mode 100644 Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html create mode 100644 Resources/Private/Templates/ViewHelpers/FluidJson/Index.html create mode 100644 Tests/Functional/Fixtures/Controller/TestController.php create mode 100644 Tests/Functional/FluidJsonTest.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..ca5bd62 --- /dev/null +++ b/.woodpecker/functional-tests.yml @@ -0,0 +1,42 @@ +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: + COMPOSER_HOME: /usr/src/app/.composer + HOSTKEY_DIGICOMP: digital-competence.de ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNSjVKJ+SO6wqmDSCgcJDk2ljWlD7qajsTxAuvZpTbJBg2++Zu0VxH0S1WzPVTD/D5UUbK6LVy6YSCnGlv6zmc0= + REPOKEY: + from_secret: deploykey + PHP_EXTENSION_PDO_SQLITE: 1 + NEOS_BUILD_DIR: /woodpecker/Build-${FLOW_VERSION} + commands: + - export HOME=/home/docker + - echo "$REPOKEY" > ~/.ssh/id_rsa + - echo "$HOSTKEY_DIGICOMP" > ~/.ssh/known_hosts + - chmod 600 ~/.ssh/id_rsa + - 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 config repositories.fluid-render-functions vcs ssh://git@digital-competence.de/Packages/DigiComp.FluidRenderFunctions + - composer remove --dev --no-update neos/behat || composer remove --no-update neos/behat + - composer require digicomp/fluid-render-functions:@dev + - composer require digicomp/fluid-json-views:@dev + - bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FluidJsonViews/Tests/Functional + when: + - event: [ push, pull_request, manual ] diff --git a/Classes/ViewHelpers/Controller/FluidJsonController.php b/Classes/ViewHelpers/Controller/FluidJsonController.php new file mode 100644 index 0000000..2f9a037 --- /dev/null +++ b/Classes/ViewHelpers/Controller/FluidJsonController.php @@ -0,0 +1,136 @@ +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()); + } + + /** + * @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); + } +} diff --git a/Classes/ViewHelpers/FluidJsonViewHelper.php b/Classes/ViewHelpers/FluidJsonViewHelper.php new file mode 100644 index 0000000..717c226 --- /dev/null +++ b/Classes/ViewHelpers/FluidJsonViewHelper.php @@ -0,0 +1,64 @@ +registerArgument('objects', QueryResult::class, 'Objects to show in table.', true); + $this->registerArgument( + 'renderFunction', + InvokeRenderFunctionInterface::class, + 'callabe to use to render single object', + true + ); + $this->registerArgument( + 'searchProperties', + 'array', + 'an array of pathes, which should be used during search evaluation', + false, + [] + ); + } + + /** + * @throws InfiniteLoopException + * @throws InvalidControllerException + * @throws MissingControllerException + * @throws StopActionException + */ + public function render(): string + { + return $this->initiateSubRequest(); + } +} diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml new file mode 100644 index 0000000..7c21f4a --- /dev/null +++ b/Configuration/Settings.yaml @@ -0,0 +1,5 @@ +DigiComp: + FluidJsonViews: + limitConfiguration: + default: 1000 + max: 1000 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..6d3229b --- /dev/null +++ b/README.md @@ -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 + + {subject.name} + + + jsonView + +``` + +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). 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..739acd0 --- /dev/null +++ b/Resources/Private/Templates/Tests/Functional/Fixtures/Test/Index.html @@ -0,0 +1,8 @@ +{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers} +{namespace fj=DigiComp\FluidJsonViews\ViewHelpers} + + {subject.name} + + + jsonView + diff --git a/Resources/Private/Templates/ViewHelpers/FluidJson/Index.html b/Resources/Private/Templates/ViewHelpers/FluidJson/Index.html new file mode 100644 index 0000000..e092c2b --- /dev/null +++ b/Resources/Private/Templates/ViewHelpers/FluidJson/Index.html @@ -0,0 +1,4 @@ + diff --git a/Tests/Functional/Fixtures/Controller/TestController.php b/Tests/Functional/Fixtures/Controller/TestController.php new file mode 100644 index 0000000..21cc511 --- /dev/null +++ b/Tests/Functional/Fixtures/Controller/TestController.php @@ -0,0 +1,17 @@ +view->assign('tags', (new Query(Tag::class))->execute()); + } +} diff --git a/Tests/Functional/FluidJsonTest.php b/Tests/Functional/FluidJsonTest.php new file mode 100644 index 0000000..2916237 --- /dev/null +++ b/Tests/Functional/FluidJsonTest.php @@ -0,0 +1,78 @@ +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']); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a44bf8e --- /dev/null +++ b/composer.json @@ -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" + ] +}