From 57d3efe1dff73b152cc0041792043961fb891ab6 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Tue, 17 Oct 2023 18:49:19 +0200 Subject: [PATCH] Initial version --- .woodpecker/code-style.yml | 8 ++ .woodpecker/functional-tests.yml | 28 +++++++ Classes/AttachmentViewTrait.php | 61 ++++++++++++++ Classes/EelFilenameTrait.php | 66 +++++++++++++++ README.md | 58 +++++++++++++ Tests/Functional/AttachmentViewTraitTest.php | 82 +++++++++++++++++++ .../Fixtures/SimpleAttachmentTemplateView.php | 33 ++++++++ composer.json | 39 +++++++++ 8 files changed, 375 insertions(+) create mode 100644 .woodpecker/code-style.yml create mode 100644 .woodpecker/functional-tests.yml create mode 100644 Classes/AttachmentViewTrait.php create mode 100644 Classes/EelFilenameTrait.php create mode 100644 README.md create mode 100644 Tests/Functional/AttachmentViewTraitTest.php create mode 100644 Tests/Functional/Fixtures/SimpleAttachmentTemplateView.php create mode 100644 composer.json diff --git a/.woodpecker/code-style.yml b/.woodpecker/code-style.yml new file mode 100644 index 0000000..bfd94b0 --- /dev/null +++ b/.woodpecker/code-style.yml @@ -0,0 +1,8 @@ +pipeline: + 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/ diff --git a/.woodpecker/functional-tests.yml b/.woodpecker/functional-tests.yml new file mode 100644 index 0000000..74fd9d1 --- /dev/null +++ b/.woodpecker/functional-tests.yml @@ -0,0 +1,28 @@ +workspace: + base: /woodpecker + path: package + +matrix: + include: + - FLOW_VERSION: 7.3 + PHP_VERSION: 8.1 + - FLOW_VERSION: 8.3 + PHP_VERSION: 8.2 + +pipeline: + 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/attachment-view-utility:@dev" + - "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.AttachmentViewUtility/Tests/Functional" diff --git a/Classes/AttachmentViewTrait.php b/Classes/AttachmentViewTrait.php new file mode 100644 index 0000000..9746f17 --- /dev/null +++ b/Classes/AttachmentViewTrait.php @@ -0,0 +1,61 @@ +supportedOptions['attachmentCharset'] = [ + 'utf-8', + 'Charset of the content or FALSE if you want to suppress the information in header', + 'string|false', + ]; + $this->supportedOptions['attachmentDisposition'] = [ + 'attachment', + 'One of "inline" or "attachment"', + 'string', + ]; + } else { + throw new \RuntimeException('supported option could not be set', 1697552694); + } + } + + public function render(): ResponseInterface + { + if ($this->options['attachmentCharset'] === false) { + $charset = ''; + } else { + $charset = '; charset=' . $this->options['attachmentCharset']; + } + return new Response( + 200, + [ + 'Content-Disposition' => ContentDisposition::create( + $this->getAttachmentName(), + true, + $this->options['attachmentDisposition'] + )->format(), + 'Content-Type' => $this->getAttachmentMimeType() . $charset, + ], + $this->getAttachmentContent() + ); + } +} diff --git a/Classes/EelFilenameTrait.php b/Classes/EelFilenameTrait.php new file mode 100644 index 0000000..3bbef2e --- /dev/null +++ b/Classes/EelFilenameTrait.php @@ -0,0 +1,66 @@ +supportedOptions['filenameEelExpression'] = [ + null, + 'Callable which creates a filename from variables', + 'string', + true, + ]; + } else { + throw new \RuntimeException('supported option could not be set', 1697552694); + } + } + + protected function getAttachmentName(): string + { + if ($this instanceof TemplateView) { + $variables = $this->getRenderingContext()->getVariableProvider()->getAll(); + } elseif (\property_exists($this, 'variables')) { + $variables = $this->variables; + } else { + throw new \RuntimeException( + 'No variables can be detected for this kind of view: ' . TypeHandling::getTypeForValue($this), + 1697550214 + ); + } + if (!\property_exists($this, 'options')) { + throw new \RuntimeException('Your view options could not be found', 1697550440); + } + $expression = $this->options[$this->eelExpressionOptionKey]; + $context = new Context(\array_merge($variables, [ + 'Array' => new ArrayHelper(), + 'String' => new StringHelper(), + 'Translation' => new TranslationHelper(), + ])); + $this->emitFilenameEelExpressionContext($context); + return (new CompilingEvaluator())->evaluate($expression, $context); + } + + /** + * @SuppressWarnings("unused") + */ + #[Flow\Signal] + protected function emitFilenameEelExpressionContext(Context $context): void + { + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..2894760 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# DigiComp.AttachmentViewUtility + +This package helps with recurring tasks of creating views for `neos/flow` projects. + +It delivers two traits: +1. `AttachmentViewTrait` creates views which returns downloadable resources +2. `EelFilenameTrait` allows to set the filename with an eel expression, which got the view variables as context + +They can be used together or standalone - but the filename trait does not make sense, if your view is not delivered by attachment. + +The `EelContext` contains the `String`, `Array` and `Translation` - Helpers. If you need more, you can connect to the `YourView::filenameEelExpressionContext` signal, which gets the context as argument. That way you can extend the context with whatever you need. + +## Example + +SimpleAttachmentView.php: +```php +class SimpleAttachmentView extends AbstractView +{ + use AttachmentViewTrait; + use EelFilenameTrait; + + public function __construct(array $options = []) + { + $this->addOptionFilenameEelExpression(); + $this->addOptionsForAttachment(); + parent::__construct($options); + } + + protected function getAttachmentContent() + { + return 'Hello ' . $this->variables['name']; + } + + protected function getAttachmentMimeType(): string + { + return 'text/plain'; + } +} +``` + +Controller: +```php +class DefaultController extends ActionController { + public function downloadAction() + { + $this->view->assign('name', 'World'); + } +} +``` + +Views.yaml: +```yaml +- + requestFilter: "isController('Default') && isAction('download')" + viewObjectName: "Acme\\Vendor\\SimpleAttachmentView" + options: + filenameEelExpression: "'hello-' + name + '.txt" +``` diff --git a/Tests/Functional/AttachmentViewTraitTest.php b/Tests/Functional/AttachmentViewTraitTest.php new file mode 100644 index 0000000..1671131 --- /dev/null +++ b/Tests/Functional/AttachmentViewTraitTest.php @@ -0,0 +1,82 @@ + 'TollesTemplate! {testVar}', + 'filenameEelExpression' => 'testVar + ".txt"', + 'attachmentCharset' => 'iso-8859-1' + ]); + $view->assign('testVar', '£ and € rates'); + + $result = $view->render(); + static::assertInstanceOf(ResponseInterface::class, $result); + static::assertEquals( + 'attachment; filename="£ and ? rates.txt"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.txt', + $result->getHeaderLine('Content-Disposition') + ); + static::assertEquals('text/plain; charset=iso-8859-1', $result->getHeaderLine('Content-Type')); + static::assertEquals('TollesTemplate! £ and € rates', (string)$result->getBody()); + } + + /** + * @test + */ + public function itIsPossibleToSuppressCharset(): void + { + $view = new SimpleAttachmentTemplateView([ + 'templateSource' => 'TollesTemplate! {testVar}', + 'filenameEelExpression' => 'testVar + ".txt"', + 'attachmentCharset' => false + ]); + $view->assign('testVar', 'WORLD'); + $result = $view->render(); + static::assertEquals('text/plain', $result->getHeaderLine('Content-Type')); + } + + /** + * @test + */ + public function itIsPossibleToExtendTheContextBySignalSlot(): void + { + $view = new SimpleAttachmentTemplateView([ + 'templateSource' => 'TollesTemplate! {testVar}', + 'filenameEelExpression' => 'greet(testVar) + ".txt"', + 'attachmentCharset' => false + ]); + $view->assign('testVar', 'WORLD'); + $dispatcher = $this->objectManager->get(Dispatcher::class); + $dispatcher->connect( + SimpleAttachmentTemplateView::class, + 'filenameEelExpressionContext', + function (Context $eelContext) { + $eelContext->push( + function ($name) { + return 'Hello ' . $name; + }, + 'greet' + ); + } + ); + $result = $view->render(); + static::assertEquals( + 'attachment; filename="Hello WORLD.txt"', + $result->getHeaderLine('Content-Disposition') + ); + } +} diff --git a/Tests/Functional/Fixtures/SimpleAttachmentTemplateView.php b/Tests/Functional/Fixtures/SimpleAttachmentTemplateView.php new file mode 100644 index 0000000..b3620f3 --- /dev/null +++ b/Tests/Functional/Fixtures/SimpleAttachmentTemplateView.php @@ -0,0 +1,33 @@ +addOptionFilenameEelExpression(); + $this->addOptionsForAttachment(); + parent::__construct($options); + } + + protected function getAttachmentContent() + { + /** @noinspection MissUsingParentKeywordInspection */ + return parent::render(); + } + + protected function getAttachmentMimeType(): string + { + return 'text/plain'; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a096c66 --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "digicomp/attachment-view-utility", + "type": "neos-package", + "description": "", + "require": { + "cardinalby/content-disposition": "^1.1", + "neos/eel": "^7.3 | ^8.3", + "neos/flow": "^7.3 | ^8.3", + "php": "^8.0" + }, + "autoload": { + "psr-4": { + "DigiComp\\AttachmentViewUtility\\": "Classes" + } + }, + "extra": { + "branch-alias": { + "dev-develop": "1.0.x-dev" + }, + "applied-flow-migrations": [ + ] + }, + "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.AttachmentViewUtility", + "keywords": [ + "Neos", + "Flow", + "view", + "mvc" + ] +}