From 0e25ea0d91ffcdf46109d3c54b4a526fd3e86971 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sat, 10 Oct 2020 07:59:43 +0200 Subject: [PATCH 01/28] Create basic QA chain --- .gitignore | 6 +++ .../AbstractComponentPresentationObject.php | 2 +- ...ractComponentPresentationObjectFactory.php | 2 +- ...sentationObjectComponentImplementation.php | 2 - Makefile | 53 +++++++++++++++++++ ...ationObjectComponentImplementationTest.php | 2 +- composer.json | 16 ++++++ phpunit.xml | 28 ++++++++++ 8 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 phpunit.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4c9708 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +Build/ +Packages/ +vendor/ +composer.lock +.phpunit.result.cache diff --git a/Classes/Fusion/AbstractComponentPresentationObject.php b/Classes/Fusion/AbstractComponentPresentationObject.php index 7899318..1fb9ab4 100755 --- a/Classes/Fusion/AbstractComponentPresentationObject.php +++ b/Classes/Fusion/AbstractComponentPresentationObject.php @@ -19,6 +19,6 @@ abstract class AbstractComponentPresentationObject implements ComponentPresentat */ final public function __call($name, $arguments) { - throw new \BadMethodCallException( '"' . $name . '" is not part of the component API for ' . __CLASS__ . '. Please check your Fusion presentation component for typos.', 1578905708); + throw new \BadMethodCallException('"' . $name . '" is not part of the component API for ' . __CLASS__ . '. Please check your Fusion presentation component for typos.', 1578905708); } } diff --git a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php index 4b6c31f..a474318 100755 --- a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php +++ b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php @@ -55,7 +55,7 @@ final protected function createWrapper(TraversableNodeInterface $node, Presentat { $wrappingService = $this->contentElementWrappingService; - return function(string $content) use($node, $fusionObject, $wrappingService) { + return function (string $content) use ($node, $fusionObject, $wrappingService) { return $wrappingService->wrapContentObject($node, $content, $fusionObject->getPath()); }; } diff --git a/Classes/Fusion/PresentationObjectComponentImplementation.php b/Classes/Fusion/PresentationObjectComponentImplementation.php index 551f068..70cda44 100755 --- a/Classes/Fusion/PresentationObjectComponentImplementation.php +++ b/Classes/Fusion/PresentationObjectComponentImplementation.php @@ -87,7 +87,6 @@ public function getContentElementFusionPath(): string && $fusionPathSegments[$numberOfFusionPathSegments - 3] === '__meta' && isset($fusionPathSegments[$numberOfFusionPathSegments - 2]) && $fusionPathSegments[$numberOfFusionPathSegments - 2] === 'process') { - // cut off the SHORT processing syntax "__meta/process/contentElementWrapping" return implode('/', array_slice($fusionPathSegments, 0, -3)); } @@ -96,7 +95,6 @@ public function getContentElementFusionPath(): string && $fusionPathSegments[$numberOfFusionPathSegments - 4] === '__meta' && isset($fusionPathSegments[$numberOfFusionPathSegments - 3]) && $fusionPathSegments[$numberOfFusionPathSegments - 3] === 'process') { - // cut off the LONG processing syntax "__meta/process/contentElementWrapping/expression" return implode('/', array_slice($fusionPathSegments, 0, -4)); } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..787932b --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +############################################################################### +############################################################################### +## ## +## PackageFactory.AtomicFusion.PresentationObjects ## +## ## +############################################################################### +############################################################################### + +############################################################################### +# VARIABLES # +############################################################################### +SHELL=/bin/bash + +############################################################################### +# INSTALL & CLEANUP # +############################################################################### +install:: + @composer install + +cleanup:: + @rm -rf Packages + @rm -rf Build + @rm -rf bin + +############################################################################### +# QA # +############################################################################### +lint:: + @bin/phpcs \ + --standard=PSR2 \ + --extensions=php \ + --exclude=Generic.Files.LineLength \ + Classes/ Tests/ + + +analyse:: + @bin/phpstan analyse \ + --autoload-file Build/BuildEssentials/PhpUnit/UnitTestBootstrap.php \ + --level 8 \ + Tests/Unit + @bin/phpstan analyse --level 8 Classes + +test:: + @bin/phpunit -c phpunit.xml \ + --enforce-time-limit \ + --coverage-html Build/Reports/coverage \ + Tests + +test-isolated:: + @bin/phpunit -c phpunit.xml \ + --enforce-time-limit \ + --group isolated \ + Tests diff --git a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php index c6dc222..a0cdd12 100644 --- a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php +++ b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php @@ -65,7 +65,7 @@ public function prepareWritesPresentationObjectToContextWhenNotInPreviewMode() $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) )) - ->will($this->returnCallback(function ($path) use($mockPresentationObject) { + ->will($this->returnCallback(function ($path) use ($mockPresentationObject) { if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { return false; } diff --git a/composer.json b/composer.json index a5c1ef8..f1f9245 100755 --- a/composer.json +++ b/composer.json @@ -13,11 +13,27 @@ "require": { "neos/fusion": "~4.2 || ~5.0 || dev-master" }, + "require-dev": { + "phpunit/phpunit": "^9.4", + "phpstan/phpstan": "^0.12.48", + "neos/buildessentials": "^6.3", + "mikey179/vfsstream": "^1.6", + "squizlabs/php_codesniffer": "^3.5" + }, + "config": { + "vendor-dir": "Packages/Libraries", + "bin-dir": "bin" + }, "autoload": { "psr-4": { "PackageFactory\\AtomicFusion\\PresentationObjects\\": "Classes" } }, + "autoload-dev": { + "psr-4": { + "PackageFactory\\AtomicFusion\\PresentationObjects\\Tests\\": "Tests" + } + }, "extra": { "neos": { "package-key": "PackageFactory.AtomicFusion.PresentationObjects" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..57db207 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + Tests/Unit + + + + + Classes + + + + + + From 07e57cf02d1c9c3031643a9ba12070dbbd2394e2 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sat, 10 Oct 2020 16:15:15 +0200 Subject: [PATCH 02/28] Fix tests and improve code coverage --- ...ractComponentPresentationObjectFactory.php | 35 +- ...sentationObjectComponentImplementation.php | 5 + Classes/Fusion/UriServiceInterface.php | 56 ++++ Classes/Infrastructure/UriService.php | 24 +- ...ComponentPresentationObjectFactoryTest.php | 225 +++++++++++++ ...bstractComponentPresentationObjectTest.php | 30 ++ ...ationObjectComponentImplementationTest.php | 62 +++- Tests/Unit/Fusion/SelfWrappingTest.php | 56 ++++ Tests/Unit/Infrastructure/UriServiceTest.php | 311 ++++++++++++++++++ composer.json | 3 +- 10 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 Classes/Fusion/UriServiceInterface.php create mode 100644 Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php create mode 100644 Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php create mode 100644 Tests/Unit/Fusion/SelfWrappingTest.php create mode 100644 Tests/Unit/Infrastructure/UriServiceTest.php diff --git a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php index a474318..2012449 100755 --- a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php +++ b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php @@ -6,6 +6,7 @@ * This file is part of the PackageFactory.AtomicFusion.PresentationObjects package */ +use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\NodeType\NodeTypeConstraintFactory; use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface; use Neos\ContentRepository\Domain\Projection\Content\TraversableNodes; @@ -14,7 +15,6 @@ use Neos\Flow\I18n\Translator; use Neos\Neos\Service\ContentElementEditableService; use Neos\Neos\Service\ContentElementWrappingService; -use PackageFactory\AtomicFusion\PresentationObjects\Infrastructure\UriService; /** * The generic abstract component presentation object factory implementation @@ -23,27 +23,27 @@ abstract class AbstractComponentPresentationObjectFactory implements ComponentPr { /** * @Flow\Inject - * @var UriService + * @var ContentElementWrappingService */ - protected $uriService; + protected $contentElementWrappingService; /** * @Flow\Inject - * @var Translator + * @var ContentElementEditableService */ - protected $translator; + protected $contentElementEditableService; /** * @Flow\Inject - * @var ContentElementEditableService + * @var UriServiceInterface */ - protected $contentElementEditableService; + protected $uriService; /** * @Flow\Inject - * @var ContentElementWrappingService + * @var Translator */ - protected $contentElementWrappingService; + protected $translator; /** * @Flow\Inject @@ -51,17 +51,29 @@ abstract class AbstractComponentPresentationObjectFactory implements ComponentPr */ protected $nodeTypeConstraintFactory; + /** + * @param TraversableNodeInterface $node + * @param PresentationObjectComponentImplementation $fusionObject + * @return callable + */ final protected function createWrapper(TraversableNodeInterface $node, PresentationObjectComponentImplementation $fusionObject): callable { $wrappingService = $this->contentElementWrappingService; return function (string $content) use ($node, $fusionObject, $wrappingService) { + /** @var NodeInterface $node */ return $wrappingService->wrapContentObject($node, $content, $fusionObject->getPath()); }; } + /** + * @param TraversableNodeInterface $node + * @param string $propertyName + * @return string + */ final protected function getEditableProperty(TraversableNodeInterface $node, string $propertyName): string { + /** @var NodeInterface $node */ return $this->contentElementEditableService->wrapContentProperty( $node, $propertyName, @@ -69,6 +81,11 @@ final protected function getEditableProperty(TraversableNodeInterface $node, str ); } + /** + * @param TraversableNodeInterface $parentNode + * @param string $nodeTypeFilterString + * @return TraversableNodes + */ final protected function findChildNodesByNodeTypeFilterString(TraversableNodeInterface $parentNode, string $nodeTypeFilterString): TraversableNodes { return $parentNode->findChildNodes($this->nodeTypeConstraintFactory->parseFilterString($nodeTypeFilterString)); diff --git a/Classes/Fusion/PresentationObjectComponentImplementation.php b/Classes/Fusion/PresentationObjectComponentImplementation.php index 70cda44..14f4be0 100755 --- a/Classes/Fusion/PresentationObjectComponentImplementation.php +++ b/Classes/Fusion/PresentationObjectComponentImplementation.php @@ -76,11 +76,15 @@ protected function getPresentationObject(): ComponentPresentationObjectInterface /** * Returns the Fusion path to the to-be-wrapped Content Element, if applicable + * (Borrowed from \Neos\Neos\Fusion\ContentElementWrappingImplementation) + * + * @TODO: We need to have a look at this one, it doesn't seem to be used anywhere (@WBE) * * @return string */ public function getContentElementFusionPath(): string { + // @codeCoverageIgnoreStart $fusionPathSegments = explode('/', $this->path); $numberOfFusionPathSegments = count($fusionPathSegments); if (isset($fusionPathSegments[$numberOfFusionPathSegments - 3]) @@ -99,5 +103,6 @@ public function getContentElementFusionPath(): string return implode('/', array_slice($fusionPathSegments, 0, -4)); } return $this->path; + // @codeCoverageIgnoreEnd } } diff --git a/Classes/Fusion/UriServiceInterface.php b/Classes/Fusion/UriServiceInterface.php new file mode 100644 index 0000000..92debd9 --- /dev/null +++ b/Classes/Fusion/UriServiceInterface.php @@ -0,0 +1,56 @@ +linkingService->createNodeUri($this->getControllerContext(), $documentNode, null, null, $absolute); } + /** + * @param string $packageKey + * @param string $resourcePath + * @return string + */ public function getResourceUri(string $packageKey, string $resourcePath): string { return $this->resourceManager->getPublicPackageResourceUri($packageKey, $resourcePath); } + /** + * @param AssetInterface $asset + * @return string + */ public function getAssetUri(AssetInterface $asset): string { return $this->resourceManager->getPublicPersistentResourceUri($asset->getResource()); } + /** + * @return string + */ public function getDummyImageBaseUri(): string { $uriBuilder = $this->getControllerContext()->getUriBuilder(); @@ -88,6 +100,9 @@ public function getDummyImageBaseUri(): string ); } + /** + * @return ControllerContext + */ public function getControllerContext(): ControllerContext { if (is_null($this->controllerContext)) { @@ -126,13 +141,14 @@ public function resolveLinkUri(string $rawLinkUri, ContentContext $subgraph): st { if (\mb_substr($rawLinkUri, 0, 7) === 'node://') { $nodeIdentifier = \mb_substr($rawLinkUri, 7); + /** @var TraversableNodeInterface $node */ $node = $subgraph->getNodeByIdentifier($nodeIdentifier); $linkUri = $node ? $this->getNodeUri($node) : '#'; } elseif (\mb_substr($rawLinkUri, 0, 8) === 'asset://') { $assetIdentifier = \mb_substr($rawLinkUri, 8); - /** @var Asset $asset */ + /** @var AssetInterface $asset */ $asset = $this->assetRepository->findByIdentifier($assetIdentifier); - $linkUri = $this->getAssetUri($asset); + $linkUri = $asset ? $this->getAssetUri($asset) : '#'; } elseif (\mb_substr($rawLinkUri, 0, 8) === 'https://' || \mb_substr($rawLinkUri, 0, 7) === 'http://') { $linkUri = $rawLinkUri; } else { diff --git a/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php new file mode 100644 index 0000000..df875eb --- /dev/null +++ b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php @@ -0,0 +1,225 @@ +prophet = new Prophet(); + + $this->contentElementWrappingService = $this->prophet->prophesize(ContentElementWrappingService::class); + $this->contentElementWrappingService + ->wrapContentObject(Argument::any(), Argument::any(), Argument::any()) + ->will(function ($args) { + $node = $args[0]; + $content = $args[1]; + $fusionPath = $args[2]; + + return vsprintf('
%s
', [ + $node->getIdentifier(), + $fusionPath, + $content + ]); + }); + + $this->contentElementEditableService = $this->prophet->prophesize(ContentElementEditableService::class); + $this->contentElementEditableService + ->wrapContentProperty(Argument::any(), Argument::any(), Argument::any()) + ->will(function ($args) { + $node = $args[0]; + $propertyName = $args[1]; + $currentValue = $args[2]; + + return vsprintf('
%s
', [ + $node->getIdentifier(), + $propertyName, + $currentValue + ]); + }); + + $this->nodeTypeConstraintFactory = $this->prophet->prophesize(NodeTypeConstraintFactory::class); + + $this->factory = new class extends AbstractComponentPresentationObjectFactory { + /** + * @param TraversableNodeInterface $node + * @param PresentationObjectComponentImplementation $fusionObject + * @return callable + */ + public function createWrapperForTest(TraversableNodeInterface $node, PresentationObjectComponentImplementation $fusionObject): callable + { + return $this->createWrapper($node, $fusionObject); + } + + /** + * @param TraversableNodeInterface $node + * @param string $propertyName + * @return string + */ + public function getEditablePropertyForTest(TraversableNodeInterface $node, string $propertyName): string + { + return $this->getEditableProperty($node, $propertyName); + } + + /** + * @param TraversableNodeInterface $parentNode + * @param string $nodeTypeFilterString + * @return TraversableNodes + */ + public function findChildNodesByNodeTypeFilterStringForTest(TraversableNodeInterface $parentNode, string $nodeTypeFilterString): TraversableNodes + { + return $this->findChildNodesByNodeTypeFilterString($parentNode, $nodeTypeFilterString); + } + }; + + $this->inject($this->factory, 'contentElementWrappingService', $this->contentElementWrappingService->reveal()); + $this->inject($this->factory, 'contentElementEditableService', $this->contentElementEditableService->reveal()); + $this->inject($this->factory, 'nodeTypeConstraintFactory', $this->nodeTypeConstraintFactory->reveal()); + } + + /** + * @after + * @return void + */ + public function tearDownComponentPresentationObjectFactory(): void + { + $this->prophet->checkPredictions(); + } + + /** + * @test + * @return void + */ + public function createsWrappersForSelfWrappingTrait(): void + { + $content = '

Lorem ipsum...

'; + + $textNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $textNode->getIdentifier()->willReturn('text-node'); + + $fusionObject = $this->prophet + ->prophesize(PresentationObjectComponentImplementation::class); + $fusionObject->getPath()->willReturn('/page/text'); + + /** @var mixed $factory */ + $factory = $this->factory; + + $wrapper = $factory->createWrapperForTest($textNode->reveal(), $fusionObject->reveal()); + + $this->assertTrue($wrapper instanceof \Closure); + + $this->assertEquals( + '

Lorem ipsum...

', + $wrapper($content) + ); + } + + /** + * @test + * @return void + */ + public function providesEditableNodeProperties(): void + { + $textNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $textNode->getIdentifier()->willReturn('text-node'); + $textNode->getProperty('content')->willReturn('

Lorem ipsum...

'); + + /** @var mixed $factory */ + $factory = $this->factory; + + $this->assertEquals( + '

Lorem ipsum...

', + $factory->getEditablePropertyForTest($textNode->reveal(), 'content') + ); + } + + /** + * @test + * @return void + */ + public function findsChildNodesByNodeTypeFilterString(): void + { + $nodeTypeFilterString = 'Neos.Neos:Document,!Neos.Neos:Shortcut'; + $constraints = new NodeTypeConstraints(false, ['Neos.Neos:Document'], ['!Neos.Neos:Shortcut']); + + $homePageNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $blogNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $aboutUsNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $imprintNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + + $result = TraversableNodes::fromArray([ + $blogNode->reveal(), + $aboutUsNode->reveal(), + $imprintNode->reveal() + ]); + + $this->nodeTypeConstraintFactory->parseFilterString($nodeTypeFilterString)->willReturn($constraints); + $homePageNode->findChildNodes($constraints)->willReturn($result); + + /** @var mixed $factory */ + $factory = $this->factory; + + $this->assertSame($result, $factory->findChildNodesByNodeTypeFilterStringForTest($homePageNode->reveal(), $nodeTypeFilterString)); + } +} diff --git a/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php b/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php new file mode 100644 index 0000000..fb84c5c --- /dev/null +++ b/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php @@ -0,0 +1,30 @@ +expectException(\BadMethodCallException::class); + + $presentationObject = new class extends AbstractComponentPresentationObject { + }; + + $presentationObject->getFoo(); + } +} diff --git a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php index a0cdd12..9adc536 100644 --- a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php +++ b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php @@ -1,17 +1,49 @@ prophet = new Prophet(); + } + + /** + * @after + * @return void + */ + public function tearDownPresentationObjectComponentImplementation(): void + { + $this->prophet->checkPredictions(); + } + /** * @test * @throws \ReflectionException @@ -100,10 +132,26 @@ protected function getPrepare(): \ReflectionMethod /** * @test - * @expectedException \PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectIsMissing + * @return void + */ + public function publishesItsOwnPath(): void + { + $subject = new PresentationObjectComponentImplementation( + $this->prophet->prophesize(Runtime::class)->reveal(), + 'path/to/button/integration', + 'Vendor.Site:Component.Button' + ); + + $this->assertEquals('path/to/button/integration', $subject->getPath()); + } + + /** + * @test */ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutGivenPresentationObject() { + $this->expectException(ComponentPresentationObjectIsMissing::class); + $mockRuntime = $this->createMock(Runtime::class); /** @var Runtime|MockObject $mockRuntime */ @@ -131,10 +179,11 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutGivenPresen /** * @test - * @expectedException \PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectInterfaceIsUndeclared */ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutDeclaredPresentationObjectInterface() { + $this->expectException(ComponentPresentationObjectInterfaceIsUndeclared::class); + $mockRuntime = $this->createMock(Runtime::class); /** @var Runtime|MockObject $mockRuntime */ @@ -166,10 +215,11 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutDeclaredPre /** * @test - * @expectedException \PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectInterfaceIsMissing */ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutExistingPresentationObjectInterface() { + $this->expectException(ComponentPresentationObjectInterfaceIsMissing::class); + $mockRuntime = $this->createMock(Runtime::class); /** @var Runtime|MockObject $mockRuntime */ @@ -201,10 +251,11 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutExistingPre /** * @test - * @expectedException \PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectDoesNotImplementRequiredInterface */ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheDeclaredInterface() { + $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); + $mockRuntime = $this->createMock(Runtime::class); /** @var Runtime|MockObject $mockRuntime */ @@ -236,10 +287,11 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationOb /** * @test - * @expectedException \PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectDoesNotImplementRequiredInterface */ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheBaseInterface() { + $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); + $mockRuntime = $this->createMock(Runtime::class); /** @var Runtime|MockObject $mockRuntime */ diff --git a/Tests/Unit/Fusion/SelfWrappingTest.php b/Tests/Unit/Fusion/SelfWrappingTest.php new file mode 100644 index 0000000..371b857 --- /dev/null +++ b/Tests/Unit/Fusion/SelfWrappingTest.php @@ -0,0 +1,56 @@ +wrapper = $wrapper; + } + }; + + $value = 'Lorem ipsum...'; + + $this->assertEquals('wrapped(Lorem ipsum...)', $selfWrappingSubject->wrap($value)); + } + + /** + * @test + * @small + * @return void + */ + public function returnsUnalteredStringValueIfWrapperIsNotSet(): void + { + $selfWrappingSubject = new class() { + use SelfWrapping; + }; + + $value = 'Lorem ipsum...'; + + $this->assertEquals('Lorem ipsum...', $selfWrappingSubject->wrap($value)); + } +} diff --git a/Tests/Unit/Infrastructure/UriServiceTest.php b/Tests/Unit/Infrastructure/UriServiceTest.php new file mode 100644 index 0000000..61545e7 --- /dev/null +++ b/Tests/Unit/Infrastructure/UriServiceTest.php @@ -0,0 +1,311 @@ +prophet = new Prophet(); + + $this->resourceManager = $this->prophet->prophesize(ResourceManager::class); + $this->linkingService = $this->prophet->prophesize(LinkingService::class); + $this->assetRepository = $this->prophet->prophesize(AssetRepository::class); + + $this->bootstrap = $this->prophet->prophesize(Bootstrap::class); + $this->bootstrap + ->getActiveRequestHandler() + ->willReturn($this->prophet->prophesize(RequestHandlerInterface::class)->reveal()); + + $this->uriBuilder = $this->prophet->prophesize(UriBuilder::class); + + $this->uriService = new UriService(); + + $this->inject($this->uriService, 'resourceManager', $this->resourceManager->reveal()); + $this->inject($this->uriService, 'linkingService', $this->linkingService->reveal()); + $this->inject($this->uriService, 'assetRepository', $this->assetRepository->reveal()); + $this->inject($this->uriService, 'bootstrap', $this->bootstrap->reveal()); + + $this->inject($this->uriService->getControllerContext(), 'uriBuilder', $this->uriBuilder->reveal()); + } + + /** + * @after + * @return void + */ + public function tearDownUriService(): void + { + $this->prophet->checkPredictions(); + } + + /** + * @test + * @return void + */ + public function providesUrisForNodes(): void + { + $documentNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + + $controllerContext = $this->uriService->getControllerContext(); + + $this->linkingService + ->createNodeUri($controllerContext, $documentNode, null, null, false) + ->willReturn('/path/to/document'); + $this->linkingService + ->createNodeUri($controllerContext, $documentNode, null, null, true) + ->willReturn('https://vendor.site/path/to/document'); + + $this->assertEquals('/path/to/document', $this->uriService->getNodeUri($documentNode->reveal(), false)); + $this->assertEquals('https://vendor.site/path/to/document', $this->uriService->getNodeUri($documentNode->reveal(), true)); + } + + /** + * @test + * @return void + */ + public function providesUrisForResources(): void + { + $this->resourceManager + ->getPublicPackageResourceUri('Vendor.Site', 'Images/logo.png') + ->willReturn('/_Resources/Static/Vendor.Site/Public/Images/logo.png'); + + $this->assertEquals( + '/_Resources/Static/Vendor.Site/Public/Images/logo.png', + $this->uriService->getResourceUri('Vendor.Site', 'Images/logo.png') + ); + } + + /** + * @test + * @return void + */ + public function providesUrisForAssets(): void + { + $resource = $this->prophet->prophesize(PersistentResource::class); + $asset = $this->prophet->prophesize(AssetInterface::class); + $asset->getResource()->willReturn($resource); + + $this->resourceManager + ->getPublicPersistentResourceUri($resource) + ->willReturn('/_Resources/Persistent/path/to/resource'); + + $this->assertEquals( + '/_Resources/Persistent/path/to/resource', + $this->uriService->getAssetUri($asset->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function providesADummyImageUri(): void + { + $this->uriBuilder + ->uriFor( + 'image', + [], + 'dummyImage', + 'Sitegeist.Kaleidoscope' + ) + ->willReturn('/path/to/dummy-image'); + + $this->assertEquals('/path/to/dummy-image', $this->uriService->getDummyImageBaseUri()); + } + + /** + * @test + * @return void + */ + public function providesAControllerContext(): void + { + $controllerContext = $this->uriService->getControllerContext(); + + $this->assertTrue($controllerContext instanceof ControllerContext); + $this->assertSame($controllerContext, $this->uriService->getControllerContext()); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithNodeProtocol(): void + { + $documentNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + + $controllerContext = $this->uriService->getControllerContext(); + + $this->linkingService + ->createNodeUri($controllerContext, $documentNode, null, null, false) + ->willReturn('/blog/2020/10/10/coronavirus-sucks.html'); + + $subgraph = $this->prophet->prophesize(ContentContext::class); + $subgraph->getNodeByIdentifier('7f2939f6-db07-476c-afac-7cac59466242')->willReturn($documentNode); + + $this->assertEquals( + '/blog/2020/10/10/coronavirus-sucks.html', + $this->uriService->resolveLinkUri('node://7f2939f6-db07-476c-afac-7cac59466242', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithNodeProtocolToHashIfNodeCannotBeFound(): void + { + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals( + '#', + $this->uriService->resolveLinkUri('node://a520cadb-eedf-42a5-b03d-796821b35e73', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithAssetProtocol(): void + { + $resource = $this->prophet->prophesize(PersistentResource::class); + $asset = $this->prophet->prophesize(AssetInterface::class); + $asset->getResource()->willReturn($resource); + + $this->resourceManager + ->getPublicPersistentResourceUri($resource) + ->willReturn('/_Resources/Persistent/path/to/49638323-a25d-43a3-a0b3-66693239439a'); + + $this->assetRepository + ->findByIdentifier('49638323-a25d-43a3-a0b3-66693239439a') + ->willReturn($asset); + + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals( + '/_Resources/Persistent/path/to/49638323-a25d-43a3-a0b3-66693239439a', + $this->uriService->resolveLinkUri('asset://49638323-a25d-43a3-a0b3-66693239439a', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithAssetProtocolToHashIfAssetCannotBeFound(): void + { + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals( + '#', + $this->uriService->resolveLinkUri('asset://49638323-a25d-43a3-a0b3-66693239439a', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithHttpProtocol(): void + { + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals( + 'http://some.domain/some/path', + $this->uriService->resolveLinkUri('http://some.domain/some/path', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisWithHttpsProtocol(): void + { + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals( + 'https://some.domain/some/path', + $this->uriService->resolveLinkUri('https://some.domain/some/path', $subgraph->reveal()) + ); + } + + /** + * @test + * @return void + */ + public function resolvesLinkUrisToHashWhenProtocolIsUnknown(): void + { + $subgraph = $this->prophet->prophesize(ContentContext::class); + + $this->assertEquals('#', $this->uriService->resolveLinkUri('ftp://some.domain/some/path', $subgraph->reveal())); + $this->assertEquals('#', $this->uriService->resolveLinkUri('#top', $subgraph->reveal())); + $this->assertEquals('#', $this->uriService->resolveLinkUri('something-cmopletely-different', $subgraph->reveal())); + } +} diff --git a/composer.json b/composer.json index f1f9245..2d8de83 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ } ], "require": { - "neos/fusion": "~4.2 || ~5.0 || dev-master" + "neos/neos": "~4.3", + "sitegeist/kaleidoscope": "^5.0" }, "require-dev": { "phpunit/phpunit": "^9.4", From 0fa66d94bd312a0b9ed3b0e3b7def7a35ffa5e00 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sat, 10 Oct 2020 18:24:45 +0200 Subject: [PATCH 03/28] Add proper type annotations and refactor so type checks are green --- .../AbstractComponentPresentationObject.php | 2 + ...ractComponentPresentationObjectFactory.php | 1 + ...sentationObjectComponentImplementation.php | 11 + Classes/Fusion/SelfWrapping.php | 6 +- Classes/Fusion/UriServiceInterface.php | 17 +- Classes/Infrastructure/UriService.php | 9 +- Makefile | 1 + ...ComponentPresentationObjectFactoryTest.php | 15 +- ...bstractComponentPresentationObjectTest.php | 1 + ...ationObjectComponentImplementationTest.php | 324 ++++++++---------- Tests/Unit/Infrastructure/UriServiceTest.php | 10 +- composer.json | 3 +- phpstan.neon | 5 + 13 files changed, 202 insertions(+), 203 deletions(-) create mode 100644 phpstan.neon diff --git a/Classes/Fusion/AbstractComponentPresentationObject.php b/Classes/Fusion/AbstractComponentPresentationObject.php index 1fb9ab4..9f8f9d6 100755 --- a/Classes/Fusion/AbstractComponentPresentationObject.php +++ b/Classes/Fusion/AbstractComponentPresentationObject.php @@ -15,7 +15,9 @@ abstract class AbstractComponentPresentationObject implements ComponentPresentat * Catches all internal EEL magic calls * * @param string $name + * @phpstan-param array $arguments * @param array $arguments + * @return void */ final public function __call($name, $arguments) { diff --git a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php index 2012449..fe60547 100755 --- a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php +++ b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php @@ -84,6 +84,7 @@ final protected function getEditableProperty(TraversableNodeInterface $node, str /** * @param TraversableNodeInterface $parentNode * @param string $nodeTypeFilterString + * @phpstan-return TraversableNodes * @return TraversableNodes */ final protected function findChildNodesByNodeTypeFilterString(TraversableNodeInterface $parentNode, string $nodeTypeFilterString): TraversableNodes diff --git a/Classes/Fusion/PresentationObjectComponentImplementation.php b/Classes/Fusion/PresentationObjectComponentImplementation.php index 14f4be0..a196d4c 100755 --- a/Classes/Fusion/PresentationObjectComponentImplementation.php +++ b/Classes/Fusion/PresentationObjectComponentImplementation.php @@ -20,7 +20,9 @@ class PresentationObjectComponentImplementation extends \Neos\Fusion\FusionObjec /** * Prepare the context for the renderer * + * @phpstan-param array $context * @param array $context + * @phpstan-return array * @return array */ protected function prepare($context) @@ -39,16 +41,25 @@ protected function prepare($context) return parent::prepare($context); } + /** + * @return string + */ public function getPath(): string { return $this->path; } + /** + * @return boolean + */ protected function isInPreviewMode(): bool { return $this->fusionValue(self::PREVIEW_MODE); } + /** + * @return ComponentPresentationObjectInterface + */ protected function getPresentationObject(): ComponentPresentationObjectInterface { $presentationObject = $this->fusionValue(self::OBJECT_NAME); diff --git a/Classes/Fusion/SelfWrapping.php b/Classes/Fusion/SelfWrapping.php index 2d00854..5cb1828 100755 --- a/Classes/Fusion/SelfWrapping.php +++ b/Classes/Fusion/SelfWrapping.php @@ -14,10 +14,14 @@ trait SelfWrapping { /** - * @var callable|null + * @var null|callable */ private $wrapper; + /** + * @param string $value + * @return string + */ final public function wrap(string $value): string { $wrapper = $this->wrapper; diff --git a/Classes/Fusion/UriServiceInterface.php b/Classes/Fusion/UriServiceInterface.php index 92debd9..d13113a 100644 --- a/Classes/Fusion/UriServiceInterface.php +++ b/Classes/Fusion/UriServiceInterface.php @@ -13,37 +13,32 @@ interface UriServiceInterface { /** - * @param string $rawLinkUri - * @param ContentContext $subgraph + * @param TraversableNodeInterface $documentNode + * @param boolean $absolute * @return string */ public function getNodeUri(TraversableNodeInterface $documentNode, bool $absolute = false): string; /** - * @param string $rawLinkUri - * @param ContentContext $subgraph + * @param string $packageKey + * @param string $resourcePath * @return string */ public function getResourceUri(string $packageKey, string $resourcePath): string; /** - * @param string $rawLinkUri - * @param ContentContext $subgraph + * @param AssetInterface $asset * @return string */ public function getAssetUri(AssetInterface $asset): string; /** - * @param string $rawLinkUri - * @param ContentContext $subgraph * @return string */ public function getDummyImageBaseUri(): string; /** - * @param string $rawLinkUri - * @param ContentContext $subgraph - * @return string + * @return ControllerContext */ public function getControllerContext(): ControllerContext; diff --git a/Classes/Infrastructure/UriService.php b/Classes/Infrastructure/UriService.php index 0e89126..2bc0f2c 100644 --- a/Classes/Infrastructure/UriService.php +++ b/Classes/Infrastructure/UriService.php @@ -48,7 +48,7 @@ final class UriService implements UriServiceInterface protected $bootstrap; /** - * @var ControllerContext + * @var null|ControllerContext */ protected $controllerContext; @@ -82,7 +82,8 @@ public function getResourceUri(string $packageKey, string $resourcePath): string */ public function getAssetUri(AssetInterface $asset): string { - return $this->resourceManager->getPublicPersistentResourceUri($asset->getResource()); + $uri = $this->resourceManager->getPublicPersistentResourceUri($asset->getResource()); + return is_string($uri) ? $uri : '#'; } /** @@ -141,12 +142,12 @@ public function resolveLinkUri(string $rawLinkUri, ContentContext $subgraph): st { if (\mb_substr($rawLinkUri, 0, 7) === 'node://') { $nodeIdentifier = \mb_substr($rawLinkUri, 7); - /** @var TraversableNodeInterface $node */ + /** @var null|TraversableNodeInterface $node */ $node = $subgraph->getNodeByIdentifier($nodeIdentifier); $linkUri = $node ? $this->getNodeUri($node) : '#'; } elseif (\mb_substr($rawLinkUri, 0, 8) === 'asset://') { $assetIdentifier = \mb_substr($rawLinkUri, 8); - /** @var AssetInterface $asset */ + /** @var null|AssetInterface $asset */ $asset = $this->assetRepository->findByIdentifier($assetIdentifier); $linkUri = $asset ? $this->getAssetUri($asset) : '#'; } elseif (\mb_substr($rawLinkUri, 0, 8) === 'https://' || \mb_substr($rawLinkUri, 0, 7) === 'http://') { diff --git a/Makefile b/Makefile index 787932b..467ceec 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ install:: @composer install cleanup:: + @rm -f composer.lock @rm -rf Packages @rm -rf Build @rm -rf bin diff --git a/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php index df875eb..5122225 100644 --- a/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php +++ b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php @@ -10,6 +10,7 @@ use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\NodeType\NodeTypeConstraintFactory; use Neos\ContentRepository\Domain\NodeType\NodeTypeConstraints; +use Neos\ContentRepository\Domain\NodeType\NodeTypeName; use Neos\ContentRepository\Domain\Projection\Content\TraversableNodes; use Neos\Neos\Service\ContentElementEditableService; use Neos\Neos\Service\ContentElementWrappingService; @@ -30,17 +31,17 @@ final class AbstractComponentPresentationObjectFactoryTest extends UnitTestCase private $prophet; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $contentElementWrappingService; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $contentElementEditableService; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $nodeTypeConstraintFactory; @@ -113,7 +114,7 @@ public function getEditablePropertyForTest(TraversableNodeInterface $node, strin /** * @param TraversableNodeInterface $parentNode * @param string $nodeTypeFilterString - * @return TraversableNodes + * @return TraversableNodes */ public function findChildNodesByNodeTypeFilterStringForTest(TraversableNodeInterface $parentNode, string $nodeTypeFilterString): TraversableNodes { @@ -193,7 +194,11 @@ public function providesEditableNodeProperties(): void public function findsChildNodesByNodeTypeFilterString(): void { $nodeTypeFilterString = 'Neos.Neos:Document,!Neos.Neos:Shortcut'; - $constraints = new NodeTypeConstraints(false, ['Neos.Neos:Document'], ['!Neos.Neos:Shortcut']); + $constraints = new NodeTypeConstraints( + false, + [NodeTypeName::fromString('Neos.Neos:Document')], + [NodeTypeName::fromString('Neos.Neos:Shortcut')] + ); $homePageNode = $this->prophet ->prophesize(TraversableNodeInterface::class) diff --git a/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php b/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php index fb84c5c..d0ce26d 100644 --- a/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php +++ b/Tests/Unit/Fusion/AbstractComponentPresentationObjectTest.php @@ -25,6 +25,7 @@ public function enforcesStructuralPropertyAccessToCircumventFaultToleranceInEel( $presentationObject = new class extends AbstractComponentPresentationObject { }; + // @phpstan-ignore-next-line $presentationObject->getFoo(); } } diff --git a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php index 9adc536..5540761 100644 --- a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php +++ b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php @@ -47,32 +47,29 @@ public function tearDownPresentationObjectComponentImplementation(): void /** * @test * @throws \ReflectionException + * @return void */ - public function prepareProperlyMergesPropsToStubbedPresentationObjectInPreviewMode() + public function prepareProperlyMergesPropsToStubbedPresentationObjectInPreviewMode(): void { - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime = $this->createMock(Runtime::class); - - $mockRuntime - ->expects($this->any()) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/foo') - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return true; - } - if ($path === 'test/foo') { - return 'bar'; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); $subject['foo'] = 'bar'; + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(true); + $runtime + ->evaluate('test/foo', $subject) + ->willReturn('bar'); + $context = $this->getPrepare()->invokeArgs($subject, [[]]); $this->assertSame(['foo' => 'bar'], $context['props']); @@ -82,39 +79,38 @@ public function prepareProperlyMergesPropsToStubbedPresentationObjectInPreviewMo /** * @test * @throws \ReflectionException + * @return void */ - public function prepareWritesPresentationObjectToContextWhenNotInPreviewMode() + public function prepareWritesPresentationObjectToContextWhenNotInPreviewMode(): void { - $mockRuntime = $this->createMock(Runtime::class); - - $mockPresentationObject = $this->createMock(ComponentPresentationObjectInterface::class); - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(3)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), - $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) - )) - ->will($this->returnCallback(function ($path) use ($mockPresentationObject) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return $mockPresentationObject; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) { - return ComponentPresentationObjectInterface::class; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $presentationObject = $this->prophet->prophesize(ComponentPresentationObjectInterface::class); + + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn($presentationObject); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) + ->willReturn(ComponentPresentationObjectInterface::class); $context = $this->getPrepare()->invokeArgs($subject, [[]]); - $this->assertSame($mockPresentationObject, $context[PresentationObjectComponentImplementation::OBJECT_NAME]); + $this->assertSame( + $presentationObject->reveal(), + $context[PresentationObjectComponentImplementation::OBJECT_NAME] + ); } /** @@ -147,176 +143,152 @@ public function publishesItsOwnPath(): void /** * @test + * @return void */ - public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutGivenPresentationObject() + public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutGivenPresentationObject(): void { $this->expectException(ComponentPresentationObjectIsMissing::class); - $mockRuntime = $this->createMock(Runtime::class); - - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(2)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME) - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return null; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn(null); $subject->evaluate(); } /** * @test + * @return void */ - public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutDeclaredPresentationObjectInterface() + public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutDeclaredPresentationObjectInterface(): void { $this->expectException(ComponentPresentationObjectInterfaceIsUndeclared::class); - $mockRuntime = $this->createMock(Runtime::class); - - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(3)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), - $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return new \DateTimeImmutable(); - } - if ($path === 'test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) { - return null; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn(new \DateTimeImmutable); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) + ->willReturn(null); $subject->evaluate(); } /** * @test + * @return void */ - public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutExistingPresentationObjectInterface() + public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutExistingPresentationObjectInterface(): void { $this->expectException(ComponentPresentationObjectInterfaceIsMissing::class); - $mockRuntime = $this->createMock(Runtime::class); - - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(3)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), - $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return new \DateTimeImmutable(); - } - if ($path === 'test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) { - return '\I\Do\Not\Exist'; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn(new \DateTimeImmutable); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) + ->willReturn('\I\Do\Not\Exist'); $subject->evaluate(); } /** * @test + * @return void */ - public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheDeclaredInterface() + public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheDeclaredInterface(): void { $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); - $mockRuntime = $this->createMock(Runtime::class); - - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(3)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), - $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return new \stdClass(); - } - if ($path === 'test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) { - return \DateTimeInterface::class; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn(new \stdClass); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) + ->willReturn(\DateTimeInterface::class); $subject->evaluate(); } /** * @test + * @return void */ - public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheBaseInterface() + public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationObjectNotImplementingTheBaseInterface(): void { $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); - $mockRuntime = $this->createMock(Runtime::class); - - /** @var Runtime|MockObject $mockRuntime */ - $mockRuntime - ->expects($this->exactly(3)) - ->method('evaluate') - ->with($this->logicalOr( - $this->equalTo('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE), - $this->equalTo('test/' . PresentationObjectComponentImplementation::OBJECT_NAME), - $this->equalTo('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) - )) - ->will($this->returnCallback(function ($path) { - if ($path === 'test/' . PresentationObjectComponentImplementation::PREVIEW_MODE) { - return false; - } - if ($path === 'test/' . PresentationObjectComponentImplementation::OBJECT_NAME) { - return new \DateTimeImmutable(); - } - if ($path === 'test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME) { - return \DateTimeInterface::class; - } - return null; - })); - - $subject = new PresentationObjectComponentImplementation($mockRuntime, 'test', 'My.Package:Component'); + $runtime = $this->prophet->prophesize(Runtime::class); + $subject = new PresentationObjectComponentImplementation( + $runtime->reveal(), + 'test', + 'My.Package:Component' + ); + + $runtime + ->getCurrentContext() + ->willReturn([]); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::PREVIEW_MODE, $subject) + ->willReturn(false); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) + ->willReturn(new \DateTimeImmutable); + $runtime + ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) + ->willReturn(\DateTimeInterface::class); $subject->evaluate(); } diff --git a/Tests/Unit/Infrastructure/UriServiceTest.php b/Tests/Unit/Infrastructure/UriServiceTest.php index 61545e7..3adfcb7 100644 --- a/Tests/Unit/Infrastructure/UriServiceTest.php +++ b/Tests/Unit/Infrastructure/UriServiceTest.php @@ -33,27 +33,27 @@ final class UriServiceTest extends UnitTestCase private $prophet; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $resourceManager; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $linkingService; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $assetRepository; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $bootstrap; /** - * @var ObjectProphecy + * @var ObjectProphecy */ private $uriBuilder; diff --git a/composer.json b/composer.json index 2d8de83..23b81cc 100755 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "phpstan/phpstan": "^0.12.48", "neos/buildessentials": "^6.3", "mikey179/vfsstream": "^1.6", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^3.5", + "jangregor/phpstan-prophecy": "^0.8.0" }, "config": { "vendor-dir": "Packages/Libraries", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..a74b9a6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +includes: + - Packages/Libraries/jangregor/phpstan-prophecy/extension.neon +parameters: + excludes_analyse: + - Tests/Unit/Helper/* \ No newline at end of file From 2de88da6d31141d2c072ddb20261e26bb962e764 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sat, 10 Oct 2020 20:22:40 +0200 Subject: [PATCH 04/28] Create qa github workflow --- .github/workflows/qa.yml | 98 ++++++++++++++++++++++++++++++++++++++++ Makefile | 3 ++ 2 files changed, 101 insertions(+) create mode 100644 .github/workflows/qa.yml diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..61a5f21 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,98 @@ +name: CI + +on: [push] + +jobs: + coding-standard: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + extensions: mbstring, intl + tools: phpcs + + - name: Run PHP Code Sniffer + run: | + phpcs \ + --standard=PSR2 \ + --extensions=php \ + --exclude=Generic.Files.LineLength \ + Classes/ Tests/ + + static-analysis: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + extensions: mbstring, intl + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: | + composer install + + - name: Run phpstan on Tests/ + run: | + bin/phpstan analyse \ + --autoload-file Build/BuildEssentials/PhpUnit/UnitTestBootstrap.php \ + --level 8 \ + Tests/Unit + + - name: Run phpstan on Classes/ + run: | + bin/phpstan analyse --level 8 Classes + + unit-tests: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + extensions: mbstring, intl + coverage: xdebug + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: | + composer install + + - name: Run unit tests + run: | + bin/phpunit -c phpunit.xml \ + --enforce-time-limit \ + --coverage-text \ + Tests \ No newline at end of file diff --git a/Makefile b/Makefile index 467ceec..d2d963c 100644 --- a/Makefile +++ b/Makefile @@ -52,3 +52,6 @@ test-isolated:: --enforce-time-limit \ --group isolated \ Tests + +github-action:: + @act -P ubuntu-20.04=shivammathur/node:focal \ No newline at end of file From 6a0b653ea5c107475b81f2fc21169db405a321df Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sat, 10 Oct 2020 20:51:11 +0200 Subject: [PATCH 05/28] Create rudimentary README --- LICENSE | 7 +++++++ README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..86bfd98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2019 Bernhard Schmitt + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e36b8df --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +![CI](https://github.com/PackageFactory/atomic-fusion-presentationobjects/workflows/CI/badge.svg?branch=release-1.0) + +# PackageFactory.AtomicFusion.PresentationObjects + +> Allows for usage of type-safe, testable presentation objects (e.g. value objects) in Atomic Fusion as a replacement for props and propsets. + +## Why + +TODO + +## Installation + +``` +composer require packagefactory/atomicfusion-presentationobjects +``` + +## Usage + +### Writing a PresentationObject + +TODO + +### Writing and registering a PresentationObject Factory + +TODO + +### Using the code generator + +TODO + +### Preview Mode + +TODO + +### Handling ContentElementWrapping in Neos + +TODO + +### Using the `UriService` + +TODO + +## License + +see [LICENSE](./LICENSE) \ No newline at end of file From be5fcbc27f27d126c36fc930e77029239596a946 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 10:25:14 +0200 Subject: [PATCH 06/28] Add .editorconfig --- .editorconfig | 27 +++++++++++++++++++++++++++ Makefile | 3 +-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..96d1f15 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_size = 2 + +[{package.json,.babelrc,.eslintrc,.stylelintrc}] +indent_size = 2 + +[*.{css,js,ts}] +indent_style = tab + +[Makefile] +indent_style = tab + +[*.makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/Makefile b/Makefile index d2d963c..aad1971 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,6 @@ lint:: --exclude=Generic.Files.LineLength \ Classes/ Tests/ - analyse:: @bin/phpstan analyse \ --autoload-file Build/BuildEssentials/PhpUnit/UnitTestBootstrap.php \ @@ -54,4 +53,4 @@ test-isolated:: Tests github-action:: - @act -P ubuntu-20.04=shivammathur/node:focal \ No newline at end of file + @act -P ubuntu-20.04=shivammathur/node:focal From eaf56126355a2456a502957d3d54cd8c567a4642 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 10:25:27 +0200 Subject: [PATCH 07/28] Add more documentation --- README.md | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e36b8df..2ff69a4 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,249 @@ > Allows for usage of type-safe, testable presentation objects (e.g. value objects) in Atomic Fusion as a replacement for props and propsets. -## Why - -TODO - ## Installation ``` composer require packagefactory/atomicfusion-presentationobjects ``` +## Why + +PackageFactory.AtomicFusion has been the first step in the direction of Component architecture in Neos CMS. It provided a `Component` fusion prototype that allowed for writing frontend components with a clear interface for the backend. + +However, that interface hasn't been strict. Developers were able to express requirements for their components, but those requirements weren't enforced on any level. + +Because of that, the concept of PropTypes were adopted from React.js in the form of PackageFactory.AtomicFusion.PropTypes. PropTypes check incoming data against a defined schema whenever a component is invoked at runtime, thus ensuring that a component can never be rendered with invalid data. + +With the advent of Typescript PropTypes have become sort-of obsolete in the React world, since static typings do not have an impact on bundle size and catch type-related bugs before runtime. + +TODO: DDD tactical pattern ValueObject + +### Benefits + +TODO + +### Drawbacks + +TODO + ## Usage ### Writing a PresentationObject +PresentationObject are ValueObjects. In that they are immutable and can only consist of scalar properties or other value objects. + +*`EXAMPLE: PresentationObject`* + +```php +firstProperty = $firstProperty; + $this->secondProperty = $secondProperty; + } + + /** + * @return string + */ + public function getFirstProperty(): string + { + return $this->firstProperty; + } + + /** + * PresentationObjects are immutable. In order to perform change actions + * you need to implement a copy-on-write mechanism like this one. + * + * Such with*-methods are optional however. + * + * @param string $firstProperty + * @return self + */ + public function withFirstProperty(string $firstProperty): self + { + return new self($firstProperty, $this->secondProperty); + } + + /** + * @return integer + */ + public function getSecondProperty(): int + { + return $this->secondProperty; + } +} +``` + +### Binding a PresentationObject to a PresentationObjectComponent + +*`EXAMPLE: PresentationObject Interface`* + +```php +*`EXAMPLE: PresentationObject Component`* + +```fusion +prototype(Vendor.Site:MyPresentationObject) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = 'Vendor\\Site\\Presentation\\MyPresentationObject\\MyPresentationObjectInterface' + + renderer = afx` +
+
First property:
+
{presentationObject.firstProperty}
+
Second property:
+
{presentationObject.secondProperty}
+
+ ` +} +``` + TODO ### Writing and registering a PresentationObject Factory +*`EXAMPLE: PresentationObject Factory`* + +```php +*`EXAMPLE: Settings.PresentationHelpers.yaml`* + +```yaml +Neos: + Fusion: + defaultContext: + Vendor.Site.MyPresentationObject: Vendor\Site\Presentation\MyPresentationObject\MyPresentationObjectFactory +``` + +TODO + +### Neos CMS content integration with PresentationObject Factories + TODO +*`EXAMPLE: MyContentElement.fusion`* + +```fusion +prototype(Vendor.Site:MyContentElement) < prototype(Neos.Neos:ContentComponent) { + renderer = Vendor.Site:MyPresentationObject { + presentationObject = ${Vendor.Site.MyPresentationObject.forNode(node)} + } +} +``` + +*`EXAMPLE: PresentationObject Factory`* + +```php +/* ... */ +final class MyPresentationObjectFactory extends AbstractComponentPresentationObjectFactory +{ + /** + * @param TraversableNodeInterface $node + * @return MyPresentationObjectInterface + */ + public function forNode(TraversableNodeInterface $node): MyPresentationObjectInterface + { + return new MyPresentationObject( + $node->getProperty('firstProperty'), + $node->getProperty('secondProperty') + ); + } +} +``` + +TODO: see Docs/Integration.md + ### Using the code generator TODO +```sh +./flow component:kickstartvalue --package-key=Vendor.Site \ + Headline \ + HeadlineLook string \ + --values=REGULAR,HERO +``` + +```sh +./flow component:kickstart --package-key=Vendor.Site + Headline \ + content:string \ + look:HeadlineLook +``` + ### Preview Mode -TODO +The `PresentationObjectComponent` has a special flag to change its behavior when used with tools like Sitegeist.Monocle. -### Handling ContentElementWrapping in Neos +Sitegeist.Monocle uses dummy data that is read directly from an annotation within the component code. That data ends up being a plain PHP array, that does not implement the desired interface. The PresentationObject enforcement would thus break Sitegeist.Monocle's component preview. -TODO +When the flag `isInPreviewMode` ist set to `true`, the default `props` context +is folded into the `presentationObject` context and the PresentationObject enforcement is deactivated. -### Using the `UriService` +This allows seamless use with tools like Sitegeist.Monocle. -TODO +*`EXAMPLE: Root.fusion`* + +```fusion +prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + isInPreviewMode = ${request.controllerPackageKey == 'Sitegeist.Monocle'} +} +``` + +> The above example shows how `isInPreviewMode` can be set to true for all PresentationObjectComponents that are rendered in Sitegeist.Monocle. ## License From dc2cebf282d2ae5cb24a9281f5c811d98b9df0ae Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Mon, 10 Feb 2020 13:57:24 +0100 Subject: [PATCH 08/28] Adjust UriService to PSR-7 changes --- Classes/Infrastructure/UriService.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Classes/Infrastructure/UriService.php b/Classes/Infrastructure/UriService.php index 2bc0f2c..2dc9390 100644 --- a/Classes/Infrastructure/UriService.php +++ b/Classes/Infrastructure/UriService.php @@ -5,6 +5,7 @@ * This file is part of the PackageFactory.AtomicFusion.PresentationObjects package. */ +use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface; use Neos\ContentRepository\Domain\Service\Context as ContentContext; use Neos\Flow\Annotations as Flow; @@ -56,7 +57,9 @@ final class UriService implements UriServiceInterface * @param TraversableNodeInterface $documentNode * @param bool $absolute * @return string + * @throws Http\Exception * @throws Mvc\Routing\Exception\MissingActionNameException + * @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException * @throws \Neos\Flow\Property\Exception * @throws \Neos\Flow\Security\Exception * @throws \Neos\Neos\Exception @@ -110,17 +113,15 @@ public function getControllerContext(): ControllerContext $requestHandler = $this->bootstrap->getActiveRequestHandler(); if ($requestHandler instanceof Http\RequestHandler) { $request = $requestHandler->getHttpRequest(); - $response = $requestHandler->getHttpResponse(); } else { - $request = Http\Request::createFromEnvironment(); - $response = new Http\Response(); + $request = ServerRequest::fromGlobals(); } - $actionRequest = new Mvc\ActionRequest($request); + $actionRequest = Mvc\ActionRequest::fromHttpRequest($request); $uriBuilder = new Mvc\Routing\UriBuilder(); $uriBuilder->setRequest($actionRequest); $this->controllerContext = new Mvc\Controller\ControllerContext( $actionRequest, - $response, + new Mvc\ActionResponse(), new Mvc\Controller\Arguments(), $uriBuilder ); @@ -133,7 +134,9 @@ public function getControllerContext(): ControllerContext * @param string $rawLinkUri * @param ContentContext $subgraph * @return string - * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException + * @throws Http\Exception + * @throws Mvc\Routing\Exception\MissingActionNameException + * @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException * @throws \Neos\Flow\Property\Exception * @throws \Neos\Flow\Security\Exception * @throws \Neos\Neos\Exception From f954bda8a814c99947d65c5e4faaedb158d3f15a Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Mon, 24 Feb 2020 00:59:27 +0100 Subject: [PATCH 09/28] WIP: Introduce component generation --- .../Command/ComponentCommandController.php | 40 +++ Classes/Domain/Component/Component.php | 247 ++++++++++++++ .../Domain/Component/ComponentGenerator.php | 72 ++++ Classes/Domain/Component/PropType.php | 82 +++++ .../Domain/Component/PropTypeIsInvalid.php | 20 ++ .../Domain/Component/PropTypeRepository.php | 67 ++++ Classes/Domain/Value/Value.php | 318 ++++++++++++++++++ Classes/Domain/Value/ValueGenerator.php | 46 +++ Tests/Unit/Domain/Component/ComponentTest.php | 241 +++++++++++++ Tests/Unit/Domain/Value/ValueTest.php | 185 ++++++++++ 10 files changed, 1318 insertions(+) create mode 100755 Classes/Command/ComponentCommandController.php create mode 100755 Classes/Domain/Component/Component.php create mode 100755 Classes/Domain/Component/ComponentGenerator.php create mode 100755 Classes/Domain/Component/PropType.php create mode 100755 Classes/Domain/Component/PropTypeIsInvalid.php create mode 100755 Classes/Domain/Component/PropTypeRepository.php create mode 100755 Classes/Domain/Value/Value.php create mode 100755 Classes/Domain/Value/ValueGenerator.php create mode 100644 Tests/Unit/Domain/Component/ComponentTest.php create mode 100644 Tests/Unit/Domain/Value/ValueTest.php diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php new file mode 100755 index 0000000..af45ed5 --- /dev/null +++ b/Classes/Command/ComponentCommandController.php @@ -0,0 +1,40 @@ +componentGenerator->generateComponent($packageKey, $componentName, $this->request->getExceedingArguments()); + } + + public function kickStartValueCommand(string $packageKey, string $componentName, string $name, string $type, array $values = null, bool $createDataSource = false): void + { + $this->valueGenerator->generateValue($packageKey, $componentName, $name, $type, $values, $createDataSource); + } +} diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php new file mode 100755 index 0000000..f45c88d --- /dev/null +++ b/Classes/Domain/Component/Component.php @@ -0,0 +1,247 @@ +findByType($propType); + } + } + $this->packageKey = $packageKey; + $this->name = $name; + $this->props = $props; + } + + public static function fromInput(string $packageKey, string $name, array $serializedProps, PropTypeRepository $propTypeRepository): self + { + $props = []; + foreach ($serializedProps as $serializedProp) { + list($propName, $serializedPropType) = explode(':', $serializedProp); + $propType = $propTypeRepository->findByType($serializedPropType); + if (is_null($propType)) { + throw PropTypeIsInvalid::becauseItIsNoKnownComponentOrPrimitive($serializedPropType); + } + $props[$propName] = $propType; + } + + return new self( + $packageKey, + $name, + $props + ); + } + + public function getPackageKey(): string + { + return $this->packageKey; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return array|PropType[] + */ + public function getProps() + { + return $this->props; + } + + public function isLeaf(): bool + { + foreach ($this->props as $propType) { + if (!$propType->isPrimitive()) { + return false; + } + } + + return true; + } + + public function getFactoryName(): string + { + return $this->getNamespace() . '\\' . $this->name . 'Factory'; + } + + public function getHelperName(): string + { + return \mb_substr($this->getPackageKey(), \mb_strrpos($this->getPackageKey(), '.') + 1) . '.' . $this->getName(); + } + + public function getInterfacePath(string $packagePath): string + { + return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . 'Interface.php'; + } + + public function getClassPath(string $packagePath): string + { + return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . '.php'; + } + + public function getFactoryPath(string $packagePath): string + { + return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . 'Factory.php'; + } + + public function getFusionPath(string $packagePath): string + { + return $packagePath . 'Resources/Private/Fusion/Presentation/' . ($this->isLeaf() ? 'Leaf' : 'Composite') . '/' . $this->name . '/' . $this->name . '.fusion'; + } + + public function getInterfaceContent(): string + { + return 'getNamespace() . '; + +/* + * This file is part of the ' . $this->getPackageKey() . ' package. + */ + +interface ' . $this->getName() . 'Interface +{ + ' . trim (implode("\n\n ", $this->getAccessors(true))) . ' +} +'; + } + + public function getClassContent(): string + { + return 'getNamespace() . '; + +/* + * This file is part of the ' . $this->getPackageKey() . ' package. + */ + +use Neos\Flow\Annotations as Flow; +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\AbstractComponentPresentationObject; + +/** + * @Flow\Proxy(false) + */ +final class ' . $this->getName() . ' extends AbstractComponentPresentationObject implements ' . $this->getName() . 'Interface +{ + ' . trim (implode("\n\n ", $this->getProperties())) . ' + + ' . $this->renderConstructor() . ' + + ' . trim (implode("\n\n ", $this->getAccessors(false))) . ' +} +'; + } + + public function getFactoryContent(): string + { + return 'getNamespace() . '; + +/* + * This file is part of the ' . $this->getPackageKey() . ' package. + */ + +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\AbstractComponentPresentationObjectFactory; + +final class ' . $this->getName() . 'Factory extends AbstractComponentPresentationObjectFactory +{ +} +'; + } + + public function getFusionContent(): string + { + $terms = []; + foreach ($this->props as $propName => $propType) { + $terms[] = '
' . $propName . ':
+
{presentationObject.' . $propName . '}
'; + } + + return 'prototype(' . $this->packageKey . ':Component.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = \'' . $this->getNamespace() . '\\' . $this->name . 'Interface\' + + renderer = afx`
+ ' . trim(implode("\n", $terms)) . ' +
` +} +'; + } + + private function getNamespace(): string + { + return \str_replace('.', '\\', $this->packageKey) . '\Presentation\\' . $this->name; + } + + private function getProperties(): array + { + $properties = []; + foreach ($this->props as $propName => $propType) { + $properties[] = '/** + * @var ' . $propType->toVar() . ' + */ + private $' . $propName . ';'; + } + + return $properties; + } + + private function renderConstructor(): string + { + $arguments = []; + $setters = []; + foreach ($this->props as $propName => $propType) { + $arguments[] = $propType->toType() . ' $' . $propName . ','; + $setters[] = '$this->' . $propName . ' = $' . $propName . ';'; + } + return 'public function __construct( + ' . trim(trim(implode("\n ", $arguments)), ',') . ' + ) { + ' . trim(implode("\n ", $setters)) . ' + }'; + } + + private function getAccessors(bool $abstract = false): array + { + $accessors = []; + foreach ($this->props as $propName => $propType) { + $accessorHeader = 'public function get' . ucfirst($propName) . '(): ' . $propType->toType(); + + $accessors[] = $accessorHeader . ($abstract ? ';' : ' + { + return $this->' . $propName . '; + }') ; + } + + return $accessors; + } +} diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php new file mode 100755 index 0000000..18bccbc --- /dev/null +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -0,0 +1,72 @@ +propTypeRepository); + + $packagePath = $this->packageManager->getPackage($packageKey)->getPackagePath(); + $classPath = $packagePath . 'Classes/Presentation/' . $componentName; + if (!file_exists($classPath)) { + Files::createDirectoryRecursively($classPath); + } + $fusionPath = $packagePath . 'Resources/Private/Fusion/Presentation/' . ($component->isLeaf() ? 'Leaf' : 'Composite') . '/' . $componentName; + if (!file_exists($fusionPath)) { + Files::createDirectoryRecursively($fusionPath); + } + file_put_contents($component->getInterfacePath($packagePath), $component->getInterfaceContent()); + file_put_contents($component->getClassPath($packagePath), $component->getClassContent()); + file_put_contents($component->getFactoryPath($packagePath), $component->getFactoryContent()); + file_put_contents($component->getFusionPath($packagePath), $component->getFusionContent()); + $this->registerFactory($component); + } + + private function registerFactory(Component $component): void + { + $configurationPath = $this->packageManager->getPackage($component->getPackageKey())->getPackagePath() . 'Configuration/'; + $configurationFilePath = $configurationPath . 'Settings.PresentationHelpers.yaml'; + if (!file_exists($configurationFilePath)) { + Files::createDirectoryRecursively($configurationPath); + $configuration = ['Neos' => ['Fusion' => ['defaultContext' => [ + $component->getHelperName() => $component->getFactoryName() + ]]]]; + } else { + $parser = new YamlParser(); + $configuration = $parser->parseFile($configurationFilePath); + $configuration['Neos']['Fusion']['defaultContext'][$component->getHelperName()] = $component->getFactoryName(); + } + + $writer = new YamlWriter(); + file_put_contents($configurationFilePath, $writer->dump($configuration, 100)); + } +} diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php new file mode 100755 index 0000000..a8acd5a --- /dev/null +++ b/Classes/Domain/Component/PropType.php @@ -0,0 +1,82 @@ +fullyQualifiedName = $fullyQualifiedName; + $this->nullable = $nullable; + } + + public static function fromType(string $type, PropTypeRepository $propTypeRepository) + { + if (!$propTypeRepository->knowsByType($type)) { + throw PropTypeIsInvalid::becauseItIsNoKnownComponentOrPrimitive($type); + } + + $values = $propTypeRepository->findValuesByType($type); + + return new self($values['fullyQualifiedName'], $values['isNullable']); + } + + public function getFullyQualifiedName(): string + { + return $this->fullyQualifiedName; + } + + public function getSimpleName(): string + { + $pivot = \mb_strrpos($this->fullyQualifiedName, '\\'); + + return \mb_substr($this->fullyQualifiedName, \mb_strrpos($this->fullyQualifiedName, $pivot ? $pivot + 1 : 0)); + } + + public function isPrimitive(): bool + { + return in_array($this->fullyQualifiedName, self::PRIMITIVES); + } + + public function isNullable(): bool + { + return $this->nullable; + } + + public function toType(): string + { + return ($this->isNullable() ? '?' : '') . $this->getSimpleName(); + } + + public function toVar(): string + { + return $this->getSimpleName() . ($this->isNullable() ? '|null' : ''); + } +} diff --git a/Classes/Domain/Component/PropTypeIsInvalid.php b/Classes/Domain/Component/PropTypeIsInvalid.php new file mode 100755 index 0000000..7c88cbe --- /dev/null +++ b/Classes/Domain/Component/PropTypeIsInvalid.php @@ -0,0 +1,20 @@ + true, + 'int' => true, + 'float' => true, + 'bool' => true + ]; + + public function findByType(string $type): ?PropType + { + if (!$this->knowsByType($type)) { + return null; + } + + return PropType::fromType($type, $this); + } + + public function findValuesByType(string $type): ?array + { + if (!$this->knowsByType($type)) { + return null; + } + + $fullyQualifiedName = $type; + $isNullable = false; + if (\mb_strpos($type, '?') === 0) { + $isNullable = true; + $fullyQualifiedName = \mb_substr($fullyQualifiedName, 1); + } + + return [ + 'fullyQualifiedName' => $fullyQualifiedName, + 'isNullable' => $isNullable + ]; + } + + public function knowsByType(string $type): bool + { + $type = trim($type, '?'); + + return isset($this->primitives[$type]); + } + + public function findSupportedPropTypes(): array + { + return array_keys($this->primitives); + } +} diff --git a/Classes/Domain/Value/Value.php b/Classes/Domain/Value/Value.php new file mode 100755 index 0000000..2a28148 --- /dev/null +++ b/Classes/Domain/Value/Value.php @@ -0,0 +1,318 @@ +packageKey = $packageKey; + $this->componentName = $componentName; + $this->name = $name; + if ($type !== 'string' && $type !== 'int') { + throw new \InvalidArgumentException('Only values of type string or int are supported at this point.', 1582502049); + } + $this->type = $type; + $this->values = $values; + } + + public function getPackageKey(): string + { + return $this->packageKey; + } + + public function getComponentName(): string + { + return $this->componentName; + } + + public function getName(): string + { + return $this->name; + } + + public function getType(): string + { + return $this->type; + } + + public function getValues(): ?array + { + return $this->values; + } + + public function getClassPath(string $packagePath): string + { + return $packagePath . 'Classes/Presentation/' . $this->componentName . '/' . $this->name . '.php'; + } + + public function getClassContent(): string + { + $variable = '$' . $this->type; + return 'getNamespace() . '; + +/* + * This file is part of the ' . $this->getPackageKey() . ' package. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class ' . $this->getName() . ' +{ + ' . $this->renderConstants() . ' + + /** + * @var ' . $this->type . ' + */ + private $value; + + private function __construct(' . $this->type . ' $value) + { + $this->value = $value; + } + + public static function from' . ucfirst($this->type) . '(' . $this->type . ' ' . $variable . '): self + { + if (!in_array(' . $variable . ', self::getValues())) { + throw ' . $this->name . 'IsInvalid::becauseItMustBeOneOfTheDefinedConstants(' . $variable . '); + } + + return new self(' . $variable . '); + } + + ' . $this->renderNamedConstructors() . ' + + ' . $this->renderComparators() . ' + + /** + * @return array|' . $this->type . '[] + */ + public static function getValues(): array + { + return [ + ' . $this->renderValues() .' + ]; + } + + public function getValue(): ' . $this->type . ' + { + return $this->value; + }' . ($this->type === 'string' ? ' + + public function __toString(): string + { + return $this->value; + }' : '') .' +} +'; + } + + public function getExceptionPath(string $packagePath): string + { + return $packagePath . 'Classes/Presentation/' . $this->componentName . '/' . $this->name . 'IsInvalid.php'; + } + + public function getExceptionContent(): string + { + return 'getNamespace() . '; + +/* + * This file is part of the ' . $this->getPackageKey() . ' package. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class ' . $this->getName() . 'IsInvalid extends \DomainException +{ + public static function becauseItMustBeOneOfTheDefinedConstants(' . $this->type . ' $attemptedValue): self + { + return new self(\'The given value "\' . $attemptedValue . \'" is no valid ' . $this->name . ', must be one of the defined constants. \', ' . time() . '); + } +} +'; + } + + public function getDataSourcePath(string $packagePath): string + { + return $packagePath . 'Classes/Application/' . $this->name . 'DataSource.php'; + } + + public function getDataSourceContent(): string + { + $arrayName = lcfirst($this->getPluralName()); + return 'getDataSourceNamespace() . '; + +/* + * This file is part of the ' . $this->packageKey . ' package. + */ + +use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\Flow\Annotations as Flow; +use Neos\Flow\I18n\Translator; +use Neos\Neos\Service\DataSource\AbstractDataSource; +use ' . $this->getNamespace() . '\\' . $this->name . '; + +class ' . $this->name . 'DataSource extends AbstractDataSource +{ + /** + * @Flow\Inject + * @var Translator + */ + protected $translator; + + /** + * @var string + */ + protected static $identifier = \'' . $this->getDataSourceIdentifier() . '\'; + + public function getData(NodeInterface $node = null, array $arguments = []): array + { + $' . $arrayName . ' = []; + foreach (' . $this->name . '::getValues() as $value) { + $' . $arrayName . '[$value][\'label\'] = $this->translator->translateById(\'' . lcfirst($this->name) . '.\' . $value, [], null, null, \'' . $this->componentName . '\', \'' . $this->packageKey . '\') ?: $value; + } + + return $' . $arrayName . '; + } +} +'; + } + + private function getPluralName(): string + { + return \mb_substr($this->name, -1) === 'y' + ? \mb_substr($this->name, 0, \mb_strlen($this->name) - 1) . 'ies' + : $this->name . 's'; + } + + private function getDataSourceIdentifier(): string + { + return strtolower(str_replace('.', '-', $this->packageKey) . '-' . implode('-', $this->splitName(true))); + } + + private function getDataSourceNamespace(): string + { + return \str_replace('.', '\\', $this->packageKey) . '\Application'; + } + + private function getNamespace(): string + { + return \str_replace('.', '\\', $this->packageKey) . '\Presentation\\' . $this->componentName; + } + + private function renderConstants(): string + { + $constants = []; + foreach ($this->values as $value) { + $constants[] = 'const ' . $this->getConstantName($value) . ' = \'' . $value . '\';'; + } + + return trim(implode("\n ", $constants)); + } + + private function renderNamedConstructors(): string + { + $constructors = []; + foreach ($this->values as $value) { + + $constructors[] = 'public static function ' . $value . '(): self + { + return new self(self::' . $this->getConstantName($value) . '); + }'; + } + + return trim(implode("\n\n ", $constructors)); + } + + private function renderComparators(): string + { + $comparators = []; + foreach ($this->values as $value) { + + $comparators[] = 'public function getIs' . ucfirst($value) . '(): bool + { + return $this->value === self::' . $this->getConstantName($value) . '; + }'; + } + + return trim(implode("\n\n ", $comparators)); + } + + public function renderValues(): string + { + $values = []; + foreach ($this->values as $value) { + $values[] = 'self::' . $this->getConstantName($value) . ','; + } + + return trim(trim(implode("\n ", $values)), ','); + } + + private function getConstantName(string $value): string + { + $parts = $this->splitName(); + if (count($parts) > 1) { + return strtoupper(end($parts) . '_' . $value); + } + + return 'VALUE_' . strtoupper($value); + } + + private function splitName(bool $plural = false): array + { + $name = $plural ? $this->getPluralName() : $this->name; + $nameParts = []; + $parts = preg_split("/([A-Z])/", $name, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + foreach ($parts as $i => $part) { + if ($i % 2 === 0) { + $nameParts[$i / 2] = $part; + } else { + $nameParts[($i - 1) / 2] .= $part; + } + } + + return $nameParts; + } +} diff --git a/Classes/Domain/Value/ValueGenerator.php b/Classes/Domain/Value/ValueGenerator.php new file mode 100755 index 0000000..1aba6b2 --- /dev/null +++ b/Classes/Domain/Value/ValueGenerator.php @@ -0,0 +1,46 @@ +packageManager->getPackage($packageKey)->getPackagePath(); + $classPath = $packagePath . 'Classes/Presentation/' . $componentName; + if (!file_exists($classPath)) { + Files::createDirectoryRecursively($classPath); + } + file_put_contents($value->getClassPath($packagePath), $value->getClassContent()); + file_put_contents($value->getExceptionPath($packagePath), $value->getExceptionContent()); + + if ($generateDataSource) { + $dataSourcePath = $packagePath . 'Classes/Application/'; + if (!is_dir($dataSourcePath)) { + Files::createDirectoryRecursively($dataSourcePath); + } + file_put_contents($value->getDataSourcePath($packagePath), $value->getDataSourceContent()); + } + } +} diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php new file mode 100644 index 0000000..e536c5e --- /dev/null +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -0,0 +1,241 @@ +subject = Component::fromInput( + 'Acme.Site', + 'MyComponent', + [ + 'bool:bool', + 'nullableBool:?bool', + 'float:float', + 'nullableFloat:?float', + 'int:int', + 'nullableInt:?int', + 'string:string', + 'nullableString:?string' + ], + new PropTypeRepository() + ); + } + + public function testGetInterfaceContent(): void + { + Assert::assertSame('subject->getInterfaceContent() + ); + } + + public function testGetClassContent(): void + { + Assert::assertSame('bool = $bool; + $this->nullableBool = $nullableBool; + $this->float = $float; + $this->nullableFloat = $nullableFloat; + $this->int = $int; + $this->nullableInt = $nullableInt; + $this->string = $string; + $this->nullableString = $nullableString; + } + + public function getBool(): bool + { + return $this->bool; + } + + public function getNullableBool(): ?bool + { + return $this->nullableBool; + } + + public function getFloat(): float + { + return $this->float; + } + + public function getNullableFloat(): ?float + { + return $this->nullableFloat; + } + + public function getInt(): int + { + return $this->int; + } + + public function getNullableInt(): ?int + { + return $this->nullableInt; + } + + public function getString(): string + { + return $this->string; + } + + public function getNullableString(): ?string + { + return $this->nullableString; + } +} +', + $this->subject->getClassContent() + ); + } + + public function testGetFactoryContent(): void + { + Assert::assertSame('subject->getFactoryContent() + ); + } + + public function testGetFusionContent(): void + { + Assert::assertSame('prototype(Acme.Site:Component.MyComponent) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = \'Acme\\Site\\Presentation\\MyComponent\\MyComponentInterface\' + + renderer = afx`
+
bool:
+
{presentationObject.bool}
+
nullableBool:
+
{presentationObject.nullableBool}
+
float:
+
{presentationObject.float}
+
nullableFloat:
+
{presentationObject.nullableFloat}
+
int:
+
{presentationObject.int}
+
nullableInt:
+
{presentationObject.nullableInt}
+
string:
+
{presentationObject.string}
+
nullableString:
+
{presentationObject.nullableString}
+
` +} +', + $this->subject->getFusionContent() + ); + } +} diff --git a/Tests/Unit/Domain/Value/ValueTest.php b/Tests/Unit/Domain/Value/ValueTest.php new file mode 100644 index 0000000..ec7010f --- /dev/null +++ b/Tests/Unit/Domain/Value/ValueTest.php @@ -0,0 +1,185 @@ +subject = new Value( + 'Acme.Site', + 'MyComponent', + 'MyComponentType', + 'string', + [ + 'primary', + 'secondary' + ] + ); + } + + public function testGetClassContent(): void + { + Assert::assertSame('value = $value; + } + + public static function fromString(string $string): self + { + if (!in_array($string, self::getValues())) { + throw MyComponentTypeIsInvalid::becauseItMustBeOneOfTheDefinedConstants($string); + } + + return new self($string); + } + + public static function primary(): self + { + return new self(self::TYPE_PRIMARY); + } + + public static function secondary(): self + { + return new self(self::TYPE_SECONDARY); + } + + public function getIsPrimary(): bool + { + return $this->value === self::TYPE_PRIMARY; + } + + public function getIsSecondary(): bool + { + return $this->value === self::TYPE_SECONDARY; + } + + /** + * @return array|string[] + */ + public static function getValues(): array + { + return [ + self::TYPE_PRIMARY, + self::TYPE_SECONDARY + ]; + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} +', + $this->subject->getClassContent() + ); + } + + public function testGetExceptionContent(): void + { + Assert::assertSame('subject->getExceptionContent() + ); + } + + public function testGetDataSourceContent(): void + { + Assert::assertSame('translator->translateById(\'myComponentType.\' . $value, [], null, null, \'MyComponent\', \'Acme.Site\') ?: $value; + } + + return $myComponentTypes; + } +} +', + $this->subject->getDataSourceContent()); + } +} From b99e026bcb1683e1a1c53f79342826ef090eea09 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 26 Feb 2020 00:53:43 +0100 Subject: [PATCH 10/28] Add package defaulting mechanisms --- .../Command/ComponentCommandController.php | 8 +-- .../Domain/Component/ComponentGenerator.php | 20 ++++---- Classes/Domain/NoPackageCouldBeResolved.php | 22 ++++++++ Classes/Domain/PackageResolver.php | 51 +++++++++++++++++++ Classes/Domain/Value/ValueGenerator.php | 13 ++--- Configuration/Settings.Fusion.yaml | 5 ++ Configuration/Settings.yaml | 10 ++-- 7 files changed, 105 insertions(+), 24 deletions(-) create mode 100755 Classes/Domain/NoPackageCouldBeResolved.php create mode 100755 Classes/Domain/PackageResolver.php create mode 100644 Configuration/Settings.Fusion.yaml diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index af45ed5..2f02805 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -28,13 +28,13 @@ class ComponentCommandController extends CommandController */ protected $valueGenerator; - public function kickStartCommand(string $packageKey, string $componentName): void + public function kickStartCommand(string $componentName, string $packageKey = null): void { - $this->componentGenerator->generateComponent($packageKey, $componentName, $this->request->getExceedingArguments()); + $this->componentGenerator->generateComponent($componentName, $this->request->getExceedingArguments(), $packageKey); } - public function kickStartValueCommand(string $packageKey, string $componentName, string $name, string $type, array $values = null, bool $createDataSource = false): void + public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = null, bool $createDataSource = false, string $packageKey = null): void { - $this->valueGenerator->generateValue($packageKey, $componentName, $name, $type, $values, $createDataSource); + $this->valueGenerator->generateValue($componentName, $name, $type, $values, $createDataSource, $packageKey); } } diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 18bccbc..8e2a14e 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -7,8 +7,9 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Package\PackageManager; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Files; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolver; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Dumper as YamlWriter; @@ -27,15 +28,16 @@ final class ComponentGenerator /** * @Flow\Inject - * @var PackageManager + * @var PackageResolver */ - protected $packageManager; + protected $packageResolver; - public function generateComponent(string $packageKey, string $componentName, array $serializedProps): void + public function generateComponent(string $componentName, array $serializedProps, ?string $packageKey = null): void { - $component = Component::fromInput($packageKey, $componentName, $serializedProps, $this->propTypeRepository); + $package = $this->packageResolver->resolvePackage($packageKey); + $component = Component::fromInput($package->getPackageKey(), $componentName, $serializedProps, $this->propTypeRepository); - $packagePath = $this->packageManager->getPackage($packageKey)->getPackagePath(); + $packagePath = $package->getPackagePath(); $classPath = $packagePath . 'Classes/Presentation/' . $componentName; if (!file_exists($classPath)) { Files::createDirectoryRecursively($classPath); @@ -48,12 +50,12 @@ public function generateComponent(string $packageKey, string $componentName, arr file_put_contents($component->getClassPath($packagePath), $component->getClassContent()); file_put_contents($component->getFactoryPath($packagePath), $component->getFactoryContent()); file_put_contents($component->getFusionPath($packagePath), $component->getFusionContent()); - $this->registerFactory($component); + $this->registerFactory($package, $component); } - private function registerFactory(Component $component): void + private function registerFactory(FlowPackageInterface $package, Component $component): void { - $configurationPath = $this->packageManager->getPackage($component->getPackageKey())->getPackagePath() . 'Configuration/'; + $configurationPath = $package->getPackagePath() . 'Configuration/'; $configurationFilePath = $configurationPath . 'Settings.PresentationHelpers.yaml'; if (!file_exists($configurationFilePath)) { Files::createDirectoryRecursively($configurationPath); diff --git a/Classes/Domain/NoPackageCouldBeResolved.php b/Classes/Domain/NoPackageCouldBeResolved.php new file mode 100755 index 0000000..7442a19 --- /dev/null +++ b/Classes/Domain/NoPackageCouldBeResolved.php @@ -0,0 +1,22 @@ +packageManager->getPackage($packageKey); + } + if ($this->defaultPackageKey) { + return $this->packageManager->getPackage($this->defaultPackageKey); + } + + foreach ($this->packageManager->getAvailablePackages() as $availablePackage) { + /** @var PackageInterface $availablePackage */ + if ($availablePackage->getComposerManifest('type') === 'neos-site') { + return $availablePackage; + } + } + + throw NoPackageCouldBeResolved::becauseNoneIsConfiguredAndNoSitePackageIsAvailable(); + } +} diff --git a/Classes/Domain/Value/ValueGenerator.php b/Classes/Domain/Value/ValueGenerator.php index 1aba6b2..c2a6b76 100755 --- a/Classes/Domain/Value/ValueGenerator.php +++ b/Classes/Domain/Value/ValueGenerator.php @@ -7,8 +7,8 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Package\PackageManager; use Neos\Utility\Files; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolver; /** * The value generator domain service @@ -19,15 +19,16 @@ final class ValueGenerator { /** * @Flow\Inject - * @var PackageManager + * @var PackageResolver */ - protected $packageManager; + protected $packageResolver; - public function generateValue(string $packageKey, string $componentName, string $name, string $type, array $values, bool $generateDataSource): void + public function generateValue(string $componentName, string $name, string $type, array $values, bool $generateDataSource, ?string $packageKey = null): void { - $value = new Value($packageKey, $componentName, $name, $type, $values); + $package = $this->packageResolver->resolvePackage($packageKey); - $packagePath = $this->packageManager->getPackage($packageKey)->getPackagePath(); + $value = new Value($package->getPackageKey(), $componentName, $name, $type, $values); + $packagePath = $package->getPackagePath(); $classPath = $packagePath . 'Classes/Presentation/' . $componentName; if (!file_exists($classPath)) { Files::createDirectoryRecursively($classPath); diff --git a/Configuration/Settings.Fusion.yaml b/Configuration/Settings.Fusion.yaml new file mode 100644 index 0000000..40f379f --- /dev/null +++ b/Configuration/Settings.Fusion.yaml @@ -0,0 +1,5 @@ +Neos: + Neos: + fusion: + autoInclude: + PackageFactory.AtomicFusion.PresentationObjects: true \ No newline at end of file diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 40f379f..c8a2b4e 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,5 +1,5 @@ -Neos: - Neos: - fusion: - autoInclude: - PackageFactory.AtomicFusion.PresentationObjects: true \ No newline at end of file +PackageFactory: + AtomicFusion: + PresentationObjects: + componentGeneration: + defaultPackageKey: '' From 022769d2dcd41fc0570aae656072178f38a663a4 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Mon, 2 Mar 2020 17:34:01 +0100 Subject: [PATCH 11/28] Add support for composite components also update test cases for newer PHPUnit versions and expose values via provider --- .../Command/ComponentCommandController.php | 8 +- Classes/Domain/Component/Component.php | 44 ++++-- .../Domain/Component/ComponentGenerator.php | 2 +- Classes/Domain/Component/PropType.php | 84 ++++++++---- Classes/Domain/Component/PropTypeClass.php | 74 +++++++++++ .../Domain/Component/PropTypeIdentifier.php | 73 ++++++++++ .../Domain/Component/PropTypeIsInvalid.php | 2 +- .../Domain/Component/PropTypeRepository.php | 96 ++++++++++---- .../Component/PropTypeRepositoryInterface.php | 20 +++ Classes/Domain/Value/Value.php | 22 ++- Classes/Domain/Value/ValueGenerator.php | 12 +- Tests/Unit/Domain/Component/ComponentTest.php | 125 +++++++++++++++++- Tests/Unit/Domain/Value/ValueTest.php | 20 ++- ...ationObjectComponentImplementationTest.php | 11 +- Tests/Unit/Helper/DummyPropTypeRepository.php | 59 +++++++++ 15 files changed, 565 insertions(+), 87 deletions(-) create mode 100755 Classes/Domain/Component/PropTypeClass.php create mode 100755 Classes/Domain/Component/PropTypeIdentifier.php create mode 100644 Classes/Domain/Component/PropTypeRepositoryInterface.php create mode 100644 Tests/Unit/Helper/DummyPropTypeRepository.php diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index 2f02805..78dbcda 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -28,13 +28,13 @@ class ComponentCommandController extends CommandController */ protected $valueGenerator; - public function kickStartCommand(string $componentName, string $packageKey = null): void + public function kickStartCommand(string $name, string $packageKey = null): void { - $this->componentGenerator->generateComponent($componentName, $this->request->getExceedingArguments(), $packageKey); + $this->componentGenerator->generateComponent($name, $this->request->getExceedingArguments(), $packageKey); } - public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = null, bool $createDataSource = false, string $packageKey = null): void + public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = null, string $packageKey = null): void { - $this->valueGenerator->generateValue($componentName, $name, $type, $values, $createDataSource, $packageKey); + $this->valueGenerator->generateValue($componentName, $name, $type, $values, $packageKey); } } diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index f45c88d..b5fa0b3 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -6,9 +6,7 @@ */ use Neos\Flow\Annotations as Flow; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropType; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeIsInvalid; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeRepository; +use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; /** * @Flow\Proxy(false) @@ -30,11 +28,11 @@ final class Component */ private $props; - public function __construct(string $packageKey, string $name, array $props, ?PropTypeRepository $propTypeRepository = null) + public function __construct(string $packageKey, string $name, array $props, ?PropTypeRepositoryInterface $propTypeRepository = null) { foreach ($props as &$propType) { if (is_string($propType)) { - $propType = $propTypeRepository->findByType($propType); + $propType = $propTypeRepository->findByType($packageKey, $name, $propType); } } $this->packageKey = $packageKey; @@ -42,14 +40,14 @@ public function __construct(string $packageKey, string $name, array $props, ?Pro $this->props = $props; } - public static function fromInput(string $packageKey, string $name, array $serializedProps, PropTypeRepository $propTypeRepository): self + public static function fromInput(string $packageKey, string $name, array $serializedProps, PropTypeRepositoryInterface $propTypeRepository): self { $props = []; foreach ($serializedProps as $serializedProp) { list($propName, $serializedPropType) = explode(':', $serializedProp); - $propType = $propTypeRepository->findByType($serializedPropType); + $propType = $propTypeRepository->findByType($packageKey, $name, $serializedPropType); if (is_null($propType)) { - throw PropTypeIsInvalid::becauseItIsNoKnownComponentOrPrimitive($serializedPropType); + throw PropTypeIsInvalid::becauseItIsNoKnownComponentValueOrPrimitive($serializedPropType); } $props[$propName] = $propType; } @@ -82,7 +80,7 @@ public function getProps() public function isLeaf(): bool { foreach ($this->props as $propType) { - if (!$propType->isPrimitive()) { + if ($propType->getClass()->isComponent()) { return false; } } @@ -129,6 +127,7 @@ public function getInterfaceContent(): string * This file is part of the ' . $this->getPackageKey() . ' package. */ +' . $this->renderUseStatements() . ' interface ' . $this->getName() . 'Interface { ' . trim (implode("\n\n ", $this->getAccessors(true))) . ' @@ -147,7 +146,7 @@ public function getClassContent(): string use Neos\Flow\Annotations as Flow; use PackageFactory\AtomicFusion\PresentationObjects\Fusion\AbstractComponentPresentationObject; - +' . $this->renderUseStatements() . ' /** * @Flow\Proxy(false) */ @@ -183,8 +182,15 @@ public function getFusionContent(): string { $terms = []; foreach ($this->props as $propName => $propType) { + if ($propType->getFullyQualifiedName() === ImageSourceHelperInterface::class) { + $definitionData = 'isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; + } elseif ($propType->getClass()->isComponent()) { + $definitionData = '<' . $this->packageKey . ':Component.' . $propType->getName() . ' presentationObject={presentationObject.' . $propName . '}' . ($propType->isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; + } else { + $definitionData = '{presentationObject.' . $propName . '}'; + } $terms[] = '
' . $propName . ':
-
{presentationObject.' . $propName . '}
'; +
' . $definitionData . '
'; } return 'prototype(' . $this->packageKey . ':Component.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @@ -215,6 +221,22 @@ private function getProperties(): array return $properties; } + private function renderUseStatements(): string + { + $statements = ''; + + $statedTypes = []; + foreach ($this->props as $propType) { + if (!$propType->getClass()->isPrimitive() && \mb_strpos($propType->getFullyQualifiedName(), $this->getNamespace()) !== 0 && !isset($statedTypes[$propType->getSimpleName()])) { + $statedTypes[$propType->getSimpleName()] = true; + $statements .= 'use ' . $propType->getFullyQualifiedName() . '; +'; + } + } + + return $statements; + } + private function renderConstructor(): string { $arguments = []; diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 8e2a14e..73d6ef9 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -69,6 +69,6 @@ private function registerFactory(FlowPackageInterface $package, Component $compo } $writer = new YamlWriter(); - file_put_contents($configurationFilePath, $writer->dump($configuration, 100)); + file_put_contents($configurationFilePath, $writer->dump($configuration, 100, 2)); } } diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php index a8acd5a..95fc40a 100755 --- a/Classes/Domain/Component/PropType.php +++ b/Classes/Domain/Component/PropType.php @@ -6,21 +6,39 @@ */ use Neos\Flow\Annotations as Flow; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeIsInvalid; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeRepository; +use Psr\Http\Message\UriInterface; +use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; /** * @Flow\Proxy(false) */ final class PropType { - const PRIMITIVES = [ - 'bool', - 'float', - 'integer', - 'string' + /** + * @var array|string[] + */ + public static $primitives = [ + 'string' => 'string', + 'int' => 'int', + 'float' => 'float', + 'bool' => 'bool' + ]; + + public static $globalValues = [ + 'ImageSource' => ImageSourceHelperInterface::class, + 'Uri' => UriInterface::class ]; + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $simpleName; + /** * @var string */ @@ -31,38 +49,48 @@ final class PropType */ private $nullable; - private function __construct(string $fullyQualifiedName, bool $nullable) + /** + * @var PropTypeClass + */ + private $class; + + private function __construct(string $name, string $simpleName, string $fullyQualifiedName, bool $nullable, PropTypeClass $class) { + $this->name = $name; + $this->simpleName = $simpleName; $this->fullyQualifiedName = $fullyQualifiedName; $this->nullable = $nullable; + $this->class = $class; } - public static function fromType(string $type, PropTypeRepository $propTypeRepository) + public static function create(string $packageKey, string $componentName, string $type, PropTypeRepositoryInterface $propTypeRepository): self { - if (!$propTypeRepository->knowsByType($type)) { - throw PropTypeIsInvalid::becauseItIsNoKnownComponentOrPrimitive($type); + if (!$identity = $propTypeRepository->findPropTypeIdentifier($packageKey, $componentName, $type)) { + throw PropTypeIsInvalid::becauseItIsNoKnownComponentValueOrPrimitive($type); } - $values = $propTypeRepository->findValuesByType($type); - - return new self($values['fullyQualifiedName'], $values['isNullable']); + return new self( + $identity->getName(), + $identity->getSimpleName(), + $identity->getFullyQualifiedName(), + $identity->isNullable(), + $identity->getClass() + ); } - public function getFullyQualifiedName(): string + public function getName(): string { - return $this->fullyQualifiedName; + return $this->name; } public function getSimpleName(): string { - $pivot = \mb_strrpos($this->fullyQualifiedName, '\\'); - - return \mb_substr($this->fullyQualifiedName, \mb_strrpos($this->fullyQualifiedName, $pivot ? $pivot + 1 : 0)); + return $this->simpleName; } - public function isPrimitive(): bool + public function getFullyQualifiedName(): string { - return in_array($this->fullyQualifiedName, self::PRIMITIVES); + return $this->fullyQualifiedName; } public function isNullable(): bool @@ -70,13 +98,23 @@ public function isNullable(): bool return $this->nullable; } + public function getClass(): PropTypeClass + { + return $this->class; + } + + public function toUse(): string + { + return $this->fullyQualifiedName; + } + public function toType(): string { - return ($this->isNullable() ? '?' : '') . $this->getSimpleName(); + return ($this->isNullable() ? '?' : '') . $this->simpleName; } public function toVar(): string { - return $this->getSimpleName() . ($this->isNullable() ? '|null' : ''); + return $this->simpleName . ($this->isNullable() ? '|null' : ''); } } diff --git a/Classes/Domain/Component/PropTypeClass.php b/Classes/Domain/Component/PropTypeClass.php new file mode 100755 index 0000000..b21f105 --- /dev/null +++ b/Classes/Domain/Component/PropTypeClass.php @@ -0,0 +1,74 @@ +value = $value; + } + + public static function primitive(): self + { + return new self(self::CLASS_PRIMITIVE); + } + + public static function globalValue(): self + { + return new self(self::CLASS_GLOBAL_VALUE); + } + + public static function value(): self + { + return new self(self::CLASS_VALUE); + } + + public static function component(): self + { + return new self(self::CLASS_COMPONENT); + } + + public function isPrimitive(): bool + { + return $this->value === self::CLASS_PRIMITIVE; + } + + public function isGlobalValue(): bool + { + return $this->value === self::CLASS_GLOBAL_VALUE; + } + + public function isValue(): bool + { + return $this->value === self::CLASS_VALUE; + } + + public function isComponent(): bool + { + return $this->value === self::CLASS_COMPONENT; + } + + public function getValue(): string + { + return $this->value; + } +} diff --git a/Classes/Domain/Component/PropTypeIdentifier.php b/Classes/Domain/Component/PropTypeIdentifier.php new file mode 100755 index 0000000..72b5baa --- /dev/null +++ b/Classes/Domain/Component/PropTypeIdentifier.php @@ -0,0 +1,73 @@ +name = $name; + $this->simpleName = $shortName; + $this->fullyQualifiedName = $fullyQualifiedName; + $this->nullable = $nullable; + $this->class = $class; + } + + public function getName(): string + { + return $this->name; + } + + public function getSimpleName(): string + { + return $this->simpleName; + } + + public function getFullyQualifiedName(): string + { + return $this->fullyQualifiedName; + } + + public function isNullable(): bool + { + return $this->nullable; + } + + public function getClass(): PropTypeClass + { + return $this->class; + } +} diff --git a/Classes/Domain/Component/PropTypeIsInvalid.php b/Classes/Domain/Component/PropTypeIsInvalid.php index 7c88cbe..7704d0d 100755 --- a/Classes/Domain/Component/PropTypeIsInvalid.php +++ b/Classes/Domain/Component/PropTypeIsInvalid.php @@ -13,7 +13,7 @@ */ class PropTypeIsInvalid extends \InvalidArgumentException { - public static function becauseItIsNoKnownComponentOrPrimitive(string $attemptedType): self + public static function becauseItIsNoKnownComponentValueOrPrimitive(string $attemptedType): self { return new self('Given prop type "' . $attemptedType . '" is invalid. It must be either a primitive or a known sub component.', 1582385578); } diff --git a/Classes/Domain/Component/PropTypeRepository.php b/Classes/Domain/Component/PropTypeRepository.php index 15e7e62..6e4ed49 100755 --- a/Classes/Domain/Component/PropTypeRepository.php +++ b/Classes/Domain/Component/PropTypeRepository.php @@ -13,55 +13,95 @@ * * @Flow\Scope("singleton") */ -final class PropTypeRepository +final class PropTypeRepository implements PropTypeRepositoryInterface { - /** - * @var array - */ - private $primitives = [ - 'string' => true, - 'int' => true, - 'float' => true, - 'bool' => true - ]; - - public function findByType(string $type): ?PropType + public function findByType(?string $packageKey, ?string $componentName, string $type): ?PropType { - if (!$this->knowsByType($type)) { + if (!$this->knowsByType($packageKey, $componentName, $type)) { return null; } - return PropType::fromType($type, $this); + return PropType::create($packageKey, $componentName, $type, $this); } - public function findValuesByType(string $type): ?array + public function findPropTypeIdentifier(string $packageKey, string $componentName, string $type): ?PropTypeIdentifier { - if (!$this->knowsByType($type)) { + if (!$this->knowsByType($packageKey, $componentName, $type)) { return null; } - $fullyQualifiedName = $type; - $isNullable = false; + $nullable = false; if (\mb_strpos($type, '?') === 0) { - $isNullable = true; - $fullyQualifiedName = \mb_substr($fullyQualifiedName, 1); + $nullable = true; + $type = \mb_substr($type, 1); } - return [ - 'fullyQualifiedName' => $fullyQualifiedName, - 'isNullable' => $isNullable - ]; + if ($this->knowsPrimitive($type)) { + return new PropTypeIdentifier($type, $type, $type, $nullable, PropTypeClass::primitive()); + } + + if ($this->knowsGlobalValue($type)) { + $className = PropType::$globalValues[$type]; + return new PropTypeIdentifier($this->getSimpleClassName($className), $this->getSimpleClassName($className), $className, $nullable, PropTypeClass::globalValue()); + } + + if ($this->knowsValue($packageKey, $componentName, $type)) { + $className = $this->getValueClassName($packageKey, $componentName, $type); + return new PropTypeIdentifier($this->getSimpleClassName($className), $this->getSimpleClassName($className), $className, $nullable, PropTypeClass::value()); + } + + if ($this->knowsComponent($packageKey, $type)) { + $interfaceName = $this->getComponentInterfaceName($packageKey, $type); + return new PropTypeIdentifier($type, $this->getSimpleClassName($interfaceName), $interfaceName, $nullable, PropTypeClass::component()); + } + + return null; + } + + private function getSimpleClassName(string $className): string + { + return \mb_substr($className, \mb_strrpos($className, '\\') + 1); } - public function knowsByType(string $type): bool + public function knowsByType(string $packageKey, string $componentName, string $type): bool { $type = trim($type, '?'); - return isset($this->primitives[$type]); + return $this->knowsPrimitive($type) + || $this->knowsGlobalValue($type) + || $this->knowsValue($packageKey, $componentName, $type) + || $this->knowsComponent($packageKey, $type); + } + + private function knowsPrimitive(string $type): bool + { + return isset(PropType::$primitives[$type]); + } + + private function knowsGlobalValue(string $type): bool + { + return isset(PropType::$globalValues[$type]); + } + + private function knowsValue(string $packageKey, string $componentName, string $type): bool + { + return class_exists($this->getValueClassName($packageKey, $componentName, $type)); + } + + private function getValueClassName(string $packageKey, string $componentName, string $type): string + { + return \str_replace('.', '\\', $packageKey) + . '\Presentation\\' . $componentName . '\\' . $type; + } + + private function knowsComponent(string $packageKey, string $type): bool + { + return class_exists($this->getComponentInterfaceName($packageKey, $type)); } - public function findSupportedPropTypes(): array + private function getComponentInterfaceName(string $packageKey, string $type): string { - return array_keys($this->primitives); + return \str_replace('.', '\\', $packageKey) + . '\Presentation\\' . $type . '\\' . $type . 'Interface'; } } diff --git a/Classes/Domain/Component/PropTypeRepositoryInterface.php b/Classes/Domain/Component/PropTypeRepositoryInterface.php new file mode 100644 index 0000000..6272f6e --- /dev/null +++ b/Classes/Domain/Component/PropTypeRepositoryInterface.php @@ -0,0 +1,20 @@ +type . '; } - public function getDataSourcePath(string $packagePath): string + public function getProviderPath(string $packagePath): string { - return $packagePath . 'Classes/Application/' . $this->name . 'DataSource.php'; + return $packagePath . 'Classes/Application/' . $this->name . 'Provider.php'; } - public function getDataSourceContent(): string + public function getProviderContent(): string { $arrayName = lcfirst($this->getPluralName()); return 'getNamespace() . '\\' . $this->name . '; -class ' . $this->name . 'DataSource extends AbstractDataSource +class ' . $this->name . 'Provider extends AbstractDataSource implements ProtectedContextAwareInterface { /** * @Flow\Inject @@ -216,6 +217,19 @@ public function getData(NodeInterface $node = null, array $arguments = []): arra return $' . $arrayName . '; } + + /** + * @return array|' . $this->type . '[] + */ + public function getValues(): array + { + return ' . $this->name . '::getValues(); + } + + public function allowsCallOfMethod($methodName): bool + { + return true; + } } '; } diff --git a/Classes/Domain/Value/ValueGenerator.php b/Classes/Domain/Value/ValueGenerator.php index c2a6b76..c20d436 100755 --- a/Classes/Domain/Value/ValueGenerator.php +++ b/Classes/Domain/Value/ValueGenerator.php @@ -23,7 +23,7 @@ final class ValueGenerator */ protected $packageResolver; - public function generateValue(string $componentName, string $name, string $type, array $values, bool $generateDataSource, ?string $packageKey = null): void + public function generateValue(string $componentName, string $name, string $type, array $values, ?string $packageKey = null): void { $package = $this->packageResolver->resolvePackage($packageKey); @@ -36,12 +36,10 @@ public function generateValue(string $componentName, string $name, string $type, file_put_contents($value->getClassPath($packagePath), $value->getClassContent()); file_put_contents($value->getExceptionPath($packagePath), $value->getExceptionContent()); - if ($generateDataSource) { - $dataSourcePath = $packagePath . 'Classes/Application/'; - if (!is_dir($dataSourcePath)) { - Files::createDirectoryRecursively($dataSourcePath); - } - file_put_contents($value->getDataSourcePath($packagePath), $value->getDataSourceContent()); + $dataSourcePath = $packagePath . 'Classes/Application/'; + if (!is_dir($dataSourcePath)) { + Files::createDirectoryRecursively($dataSourcePath); } + file_put_contents($value->getProviderPath($packagePath), $value->getProviderContent()); } } diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php index e536c5e..3ff5d09 100644 --- a/Tests/Unit/Domain/Component/ComponentTest.php +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -3,7 +3,9 @@ use Neos\Flow\Tests\UnitTestCase; use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\Component; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeRepository; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeClass; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\Component\PropTypeIdentifier; +use PackageFactory\AtomicFusion\PresentationObjects\Tests\Unit\Helper\DummyPropTypeRepository; use PHPUnit\Framework\Assert; /** @@ -20,6 +22,12 @@ public function setUp(): void { parent::setUp(); + $propTypeRepository = new DummyPropTypeRepository(); + $propTypeRepository->propTypeIdentifiers['Acme.Site']['MySubComponent'] = [ + 'MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', false, PropTypeClass::component()), + '?MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', true, PropTypeClass::component()) + ]; + $this->subject = Component::fromInput( 'Acme.Site', 'MyComponent', @@ -31,9 +39,15 @@ public function setUp(): void 'int:int', 'nullableInt:?int', 'string:string', - 'nullableString:?string' + 'nullableString:?string', + 'uri:Uri', + 'nullableUri:?Uri', + 'image:ImageSource', + 'nullableImage:?ImageSource', + 'subComponent:MySubComponent', + 'nullableSubComponent:?MySubComponent' ], - new PropTypeRepository() + $propTypeRepository ); } @@ -46,6 +60,10 @@ public function testGetInterfaceContent(): void * This file is part of the Acme.Site package. */ +use Psr\Http\Message\UriInterface; +use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; +use Acme\Site\Presentation\MySubComponent\MySubComponentInterface; + interface MyComponentInterface { public function getBool(): bool; @@ -63,6 +81,18 @@ public function getNullableInt(): ?int; public function getString(): string; public function getNullableString(): ?string; + + public function getUri(): UriInterface; + + public function getNullableUri(): ?UriInterface; + + public function getImage(): ImageSourceHelperInterface; + + public function getNullableImage(): ?ImageSourceHelperInterface; + + public function getSubComponent(): MySubComponentInterface; + + public function getNullableSubComponent(): ?MySubComponentInterface; } ', $this->subject->getInterfaceContent() @@ -80,6 +110,9 @@ public function testGetClassContent(): void use Neos\Flow\Annotations as Flow; use PackageFactory\AtomicFusion\PresentationObjects\Fusion\AbstractComponentPresentationObject; +use Psr\Http\Message\UriInterface; +use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; +use Acme\Site\Presentation\MySubComponent\MySubComponentInterface; /** * @Flow\Proxy(false) @@ -126,6 +159,36 @@ final class MyComponent extends AbstractComponentPresentationObject implements M */ private $nullableString; + /** + * @var UriInterface + */ + private $uri; + + /** + * @var UriInterface|null + */ + private $nullableUri; + + /** + * @var ImageSourceHelperInterface + */ + private $image; + + /** + * @var ImageSourceHelperInterface|null + */ + private $nullableImage; + + /** + * @var MySubComponentInterface + */ + private $subComponent; + + /** + * @var MySubComponentInterface|null + */ + private $nullableSubComponent; + public function __construct( bool $bool, ?bool $nullableBool, @@ -134,7 +197,13 @@ public function __construct( int $int, ?int $nullableInt, string $string, - ?string $nullableString + ?string $nullableString, + UriInterface $uri, + ?UriInterface $nullableUri, + ImageSourceHelperInterface $image, + ?ImageSourceHelperInterface $nullableImage, + MySubComponentInterface $subComponent, + ?MySubComponentInterface $nullableSubComponent ) { $this->bool = $bool; $this->nullableBool = $nullableBool; @@ -144,6 +213,12 @@ public function __construct( $this->nullableInt = $nullableInt; $this->string = $string; $this->nullableString = $nullableString; + $this->uri = $uri; + $this->nullableUri = $nullableUri; + $this->image = $image; + $this->nullableImage = $nullableImage; + $this->subComponent = $subComponent; + $this->nullableSubComponent = $nullableSubComponent; } public function getBool(): bool @@ -185,6 +260,36 @@ public function getNullableString(): ?string { return $this->nullableString; } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function getNullableUri(): ?UriInterface + { + return $this->nullableUri; + } + + public function getImage(): ImageSourceHelperInterface + { + return $this->image; + } + + public function getNullableImage(): ?ImageSourceHelperInterface + { + return $this->nullableImage; + } + + public function getSubComponent(): MySubComponentInterface + { + return $this->subComponent; + } + + public function getNullableSubComponent(): ?MySubComponentInterface + { + return $this->nullableSubComponent; + } } ', $this->subject->getClassContent() @@ -232,6 +337,18 @@ public function testGetFusionContent(): void
{presentationObject.string}
nullableString:
{presentationObject.nullableString}
+
uri:
+
{presentationObject.uri}
+
nullableUri:
+
{presentationObject.nullableUri}
+
image:
+
+
nullableImage:
+
+
subComponent:
+
+
nullableSubComponent:
+
` } ', diff --git a/Tests/Unit/Domain/Value/ValueTest.php b/Tests/Unit/Domain/Value/ValueTest.php index ec7010f..8bd4768 100644 --- a/Tests/Unit/Domain/Value/ValueTest.php +++ b/Tests/Unit/Domain/Value/ValueTest.php @@ -141,7 +141,7 @@ public static function becauseItMustBeOneOfTheDefinedConstants(string $attempted ); } - public function testGetDataSourceContent(): void + public function testGetProviderContent(): void { Assert::assertSame('subject->getDataSourceContent()); + $this->subject->getProviderContent()); } } diff --git a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php index 5540761..96653a2 100644 --- a/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php +++ b/Tests/Unit/Fusion/PresentationObjectComponentImplementationTest.php @@ -13,7 +13,6 @@ use PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectInterfaceIsUndeclared; use PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectIsMissing; use PackageFactory\AtomicFusion\PresentationObjects\Fusion\PresentationObjectComponentImplementation; -use PHPUnit\Framework\MockObject\MockObject; use Prophecy\Prophet; /** @@ -166,6 +165,8 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutGivenPresen ->evaluate('test/' . PresentationObjectComponentImplementation::OBJECT_NAME, $subject) ->willReturn(null); + $this->expectException(ComponentPresentationObjectIsMissing::class); + $subject->evaluate(); } @@ -197,6 +198,8 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutDeclaredPre ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) ->willReturn(null); + $this->expectException(ComponentPresentationObjectInterfaceIsUndeclared::class); + $subject->evaluate(); } @@ -228,6 +231,8 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithoutExistingPre ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) ->willReturn('\I\Do\Not\Exist'); + $this->expectException(ComponentPresentationObjectInterfaceIsMissing::class); + $subject->evaluate(); } @@ -259,6 +264,8 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationOb ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) ->willReturn(\DateTimeInterface::class); + $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); + $subject->evaluate(); } @@ -290,6 +297,8 @@ public function evaluateThrowsExceptionWhenNotInPreviewModeAndWithPresentationOb ->evaluate('test/' . PresentationObjectComponentImplementation::INTERFACE_DECLARATION_NAME, $subject) ->willReturn(\DateTimeInterface::class); + $this->expectException(ComponentPresentationObjectDoesNotImplementRequiredInterface::class); + $subject->evaluate(); } } diff --git a/Tests/Unit/Helper/DummyPropTypeRepository.php b/Tests/Unit/Helper/DummyPropTypeRepository.php new file mode 100644 index 0000000..62cacda --- /dev/null +++ b/Tests/Unit/Helper/DummyPropTypeRepository.php @@ -0,0 +1,59 @@ +realPropTypeRepository = new PropTypeRepository(); + } + + public function findByType(?string $packageKey, ?string $componentName, string $type): ?PropType + { + if (!$this->knowsByType($packageKey, $componentName, $type)) { + return null; + } + + return PropType::create($packageKey, $componentName, $type, $this); + } + + public function findPropTypeIdentifier(string $packageKey, string $componentName, string $type): ?PropTypeIdentifier + { + $alternativeComponentName = trim($type, '?'); + + return $this->realPropTypeRepository->findPropTypeIdentifier($packageKey, $componentName, $type) + ?: ($this->propTypeIdentifiers[$packageKey][$componentName][$type] ?? ($this->propTypeIdentifiers[$packageKey][$alternativeComponentName][$type] ?? null)); + } + + public function knowsByType(string $packageKey, string $componentName, string $type): bool + { + $alternativeComponentName = trim($type, '?'); + + return $this->realPropTypeRepository->knowsByType($packageKey, $componentName, $type) + || isset($this->propTypeIdentifiers[$packageKey][$componentName][$type]) + || isset($this->propTypeIdentifiers[$packageKey][$alternativeComponentName][$type]); + } +} From d612136e66d9b7fa54a2de31a378fd518ca0a10e Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Mon, 27 Apr 2020 15:05:07 +0200 Subject: [PATCH 12/28] Support styleguide annotations --- Classes/Domain/Component/Component.php | 12 +++++- Classes/Domain/Component/PropType.php | 40 +++++++++++++++++++ Tests/Unit/Domain/Component/ComponentTest.php | 29 ++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index b5fa0b3..7482dfa 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -181,6 +181,7 @@ final class ' . $this->getName() . 'Factory extends AbstractComponentPresentatio public function getFusionContent(): string { $terms = []; + $styleGuideProps = []; foreach ($this->props as $propName => $propType) { if ($propType->getFullyQualifiedName() === ImageSourceHelperInterface::class) { $definitionData = 'isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; @@ -189,13 +190,22 @@ public function getFusionContent(): string } else { $definitionData = '{presentationObject.' . $propName . '}'; } - $terms[] = '
' . $propName . ':
+ $styleGuideProps[] = $propName . ' = ' . $propType->toStyleGuidePropValue(); + $terms[] = '
' . $propName . ':
' . $definitionData . '
'; } return 'prototype(' . $this->packageKey . ':Component.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @presentationObjectInterface = \'' . $this->getNamespace() . '\\' . $this->name . 'Interface\' + @styleguide { + title = \'' . $this->name . '\' + + props { + ' . implode("\n ", $styleGuideProps) .' + } + } + renderer = afx`
' . trim(implode("\n", $terms)) . '
` diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php index 95fc40a..f603f97 100755 --- a/Classes/Domain/Component/PropType.php +++ b/Classes/Domain/Component/PropType.php @@ -117,4 +117,44 @@ public function toVar(): string { return $this->simpleName . ($this->isNullable() ? '|null' : ''); } + + public function toStyleGuidePropValue(): string + { + $styleGuideValue = ''; + if ($this->class->isPrimitive()) { + switch ($this->name) { + case 'string': + $styleGuideValue = '\'Text\''; + break; + case 'int': + $styleGuideValue = '4711'; + break; + case 'float': + $styleGuideValue = '47.11'; + break; + case 'bool': + $styleGuideValue = 'true'; + break; + } + } elseif ($this->class->isGlobalValue()) { + switch ($this->name) { + case 'ImageSourceHelperInterface': + $styleGuideValue = 'Sitegeist.Kaleidoscope:DummyImageSource { + height = 1920 + width = 1080 + }'; + break; + case 'UriInterface': + $styleGuideValue = '\'https://neos.io\''; + break; + } + } elseif ($this->class->isValue()) { + $styleGuideValue = ''; + } elseif ($this->class->isComponent()) { + $styleGuideValue = '{ + }'; + } + + return $styleGuideValue; + } } diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php index 3ff5d09..6cc996a 100644 --- a/Tests/Unit/Domain/Component/ComponentTest.php +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -320,6 +320,35 @@ public function testGetFusionContent(): void Assert::assertSame('prototype(Acme.Site:Component.MyComponent) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @presentationObjectInterface = \'Acme\\Site\\Presentation\\MyComponent\\MyComponentInterface\' + @styleguide { + title = \'MyComponent\' + + props { + bool = true + nullableBool = true + float = 47.11 + nullableFloat = 47.11 + int = 4711 + nullableInt = 4711 + string = \'Text\' + nullableString = \'Text\' + uri = \'https://neos.io\' + nullableUri = \'https://neos.io\' + image = Sitegeist.Kaleidoscope:DummyImageSource { + height = 1920 + width = 1080 + } + nullableImage = Sitegeist.Kaleidoscope:DummyImageSource { + height = 1920 + width = 1080 + } + subComponent = { + } + nullableSubComponent = { + } + } + } + renderer = afx`
bool:
{presentationObject.bool}
From 720cad1eb4b128bb03d9bce8249ca013420672d2 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Tue, 28 Apr 2020 11:06:04 +0200 Subject: [PATCH 13/28] Properly indent yaml files --- Classes/Domain/Component/ComponentGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 73d6ef9..90e932b 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -68,7 +68,7 @@ private function registerFactory(FlowPackageInterface $package, Component $compo $configuration['Neos']['Fusion']['defaultContext'][$component->getHelperName()] = $component->getFactoryName(); } - $writer = new YamlWriter(); - file_put_contents($configurationFilePath, $writer->dump($configuration, 100, 2)); + $writer = new YamlWriter(2); + file_put_contents($configurationFilePath, $writer->dump($configuration, 100)); } } From a4f4b4b79bc946212336d77503f1b8000ed5e05e Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Tue, 28 Apr 2020 11:06:36 +0200 Subject: [PATCH 14/28] Check component existence against interfaces instead of classes --- Classes/Domain/Component/PropTypeRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Domain/Component/PropTypeRepository.php b/Classes/Domain/Component/PropTypeRepository.php index 6e4ed49..c06ea0a 100755 --- a/Classes/Domain/Component/PropTypeRepository.php +++ b/Classes/Domain/Component/PropTypeRepository.php @@ -96,7 +96,7 @@ private function getValueClassName(string $packageKey, string $componentName, st private function knowsComponent(string $packageKey, string $type): bool { - return class_exists($this->getComponentInterfaceName($packageKey, $type)); + return interface_exists($this->getComponentInterfaceName($packageKey, $type)); } private function getComponentInterfaceName(string $packageKey, string $type): string From 59013195c3eaf4319a3d5bc53ca66d7817c26bb7 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 29 Apr 2020 14:31:32 +0200 Subject: [PATCH 15/28] Introduce component types and fix Fusion rendering --- Classes/Domain/Component/Component.php | 22 +++++-- .../Domain/Component/ComponentGenerator.php | 2 +- .../Domain/Component/ComponentRepository.php | 33 +++++++++++ Classes/Domain/Component/ComponentType.php | 57 +++++++++++++++++++ Classes/Domain/Component/PropType.php | 14 ++--- Classes/Domain/Component/PropTypeClass.php | 30 ++++++++-- .../Domain/Component/PropTypeRepository.php | 15 ++++- Tests/Unit/Domain/Component/ComponentTest.php | 17 +++--- 8 files changed, 164 insertions(+), 26 deletions(-) create mode 100755 Classes/Domain/Component/ComponentRepository.php create mode 100755 Classes/Domain/Component/ComponentType.php diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index 7482dfa..68a8f24 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -88,6 +88,17 @@ public function isLeaf(): bool return true; } + public function getType(): ComponentType + { + foreach ($this->props as $propType) { + if ($propType->getClass()->isComponent()) { + return ComponentType::composite(); + } + } + + return ComponentType::leaf(); + } + public function getFactoryName(): string { return $this->getNamespace() . '\\' . $this->name . 'Factory'; @@ -115,7 +126,7 @@ public function getFactoryPath(string $packagePath): string public function getFusionPath(string $packagePath): string { - return $packagePath . 'Resources/Private/Fusion/Presentation/' . ($this->isLeaf() ? 'Leaf' : 'Composite') . '/' . $this->name . '/' . $this->name . '.fusion'; + return $packagePath . 'Resources/Private/Fusion/Presentation/' . ucfirst($this->getType()) . '/' . $this->name . '/' . $this->name . '.fusion'; } public function getInterfaceContent(): string @@ -127,8 +138,9 @@ public function getInterfaceContent(): string * This file is part of the ' . $this->getPackageKey() . ' package. */ +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectInterface; ' . $this->renderUseStatements() . ' -interface ' . $this->getName() . 'Interface +interface ' . $this->getName() . 'Interface extends ComponentPresentationObjectInterface { ' . trim (implode("\n\n ", $this->getAccessors(true))) . ' } @@ -186,16 +198,16 @@ public function getFusionContent(): string if ($propType->getFullyQualifiedName() === ImageSourceHelperInterface::class) { $definitionData = 'isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; } elseif ($propType->getClass()->isComponent()) { - $definitionData = '<' . $this->packageKey . ':Component.' . $propType->getName() . ' presentationObject={presentationObject.' . $propName . '}' . ($propType->isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; + $definitionData = '<' . $this->packageKey . ':' . ucfirst($propType->getClass()) . '.' . $propType->getName() . ' presentationObject={presentationObject.' . $propName . '}' . ($propType->isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; } else { $definitionData = '{presentationObject.' . $propName . '}'; } - $styleGuideProps[] = $propName . ' = ' . $propType->toStyleGuidePropValue(); + $styleGuideProps[] = $propName . ' ' . $propType->toStyleGuidePropValue(); $terms[] = '
' . $propName . ':
' . $definitionData . '
'; } - return 'prototype(' . $this->packageKey . ':Component.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + return 'prototype(' . $this->packageKey . ':' . ucfirst($this->getType()) . '.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @presentationObjectInterface = \'' . $this->getNamespace() . '\\' . $this->name . 'Interface\' @styleguide { diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 90e932b..c1dc11b 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -42,7 +42,7 @@ public function generateComponent(string $componentName, array $serializedProps, if (!file_exists($classPath)) { Files::createDirectoryRecursively($classPath); } - $fusionPath = $packagePath . 'Resources/Private/Fusion/Presentation/' . ($component->isLeaf() ? 'Leaf' : 'Composite') . '/' . $componentName; + $fusionPath = $packagePath . 'Resources/Private/Fusion/Presentation/' . ucfirst($component->getType()) . '/' . $componentName; if (!file_exists($fusionPath)) { Files::createDirectoryRecursively($fusionPath); } diff --git a/Classes/Domain/Component/ComponentRepository.php b/Classes/Domain/Component/ComponentRepository.php new file mode 100755 index 0000000..4f58224 --- /dev/null +++ b/Classes/Domain/Component/ComponentRepository.php @@ -0,0 +1,33 @@ +getMethods() as $method) { + if (\mb_strpos($method->getName(), 'get') === 0 && interface_exists((string) $method->getReturnType())) { + $reflection = new \ReflectionClass((string) $method->getReturnType()); + if (in_array(ComponentPresentationObjectInterface::class, $reflection->getInterfaceNames())) { + return ComponentType::composite(); + } + } + } + + return ComponentType::leaf(); + } +} diff --git a/Classes/Domain/Component/ComponentType.php b/Classes/Domain/Component/ComponentType.php new file mode 100755 index 0000000..1f94f6e --- /dev/null +++ b/Classes/Domain/Component/ComponentType.php @@ -0,0 +1,57 @@ +value = $value; + } + + public static function leaf(): self + { + return new self(self::TYPE_LEAF); + } + + public static function composite(): self + { + return new self(self::TYPE_COMPOSITE); + } + + public function isLeaf(): bool + { + return $this->value === self::TYPE_LEAF; + } + + public function isComposite(): bool + { + return $this->value === self::TYPE_COMPOSITE; + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php index f603f97..3c9807e 100755 --- a/Classes/Domain/Component/PropType.php +++ b/Classes/Domain/Component/PropType.php @@ -124,32 +124,32 @@ public function toStyleGuidePropValue(): string if ($this->class->isPrimitive()) { switch ($this->name) { case 'string': - $styleGuideValue = '\'Text\''; + $styleGuideValue = '= \'Text\''; break; case 'int': - $styleGuideValue = '4711'; + $styleGuideValue = '= 4711'; break; case 'float': - $styleGuideValue = '47.11'; + $styleGuideValue = '= 47.11'; break; case 'bool': - $styleGuideValue = 'true'; + $styleGuideValue = '= true'; break; } } elseif ($this->class->isGlobalValue()) { switch ($this->name) { case 'ImageSourceHelperInterface': - $styleGuideValue = 'Sitegeist.Kaleidoscope:DummyImageSource { + $styleGuideValue = '= Sitegeist.Kaleidoscope:DummyImageSource { height = 1920 width = 1080 }'; break; case 'UriInterface': - $styleGuideValue = '\'https://neos.io\''; + $styleGuideValue = '= \'https://neos.io\''; break; } } elseif ($this->class->isValue()) { - $styleGuideValue = ''; + $styleGuideValue = '= \'\''; } elseif ($this->class->isComponent()) { $styleGuideValue = '{ }'; diff --git a/Classes/Domain/Component/PropTypeClass.php b/Classes/Domain/Component/PropTypeClass.php index b21f105..4319ec0 100755 --- a/Classes/Domain/Component/PropTypeClass.php +++ b/Classes/Domain/Component/PropTypeClass.php @@ -15,7 +15,8 @@ final class PropTypeClass const CLASS_PRIMITIVE = 'primitive'; const CLASS_GLOBAL_VALUE = 'globalValue'; const CLASS_VALUE = 'value'; - const CLASS_COMPONENT = 'component'; + const CLASS_COMPOSITE = 'composite'; + const CLASS_LEAF = 'leaf'; /** * @var string @@ -42,9 +43,14 @@ public static function value(): self return new self(self::CLASS_VALUE); } - public static function component(): self + public static function leaf(): self { - return new self(self::CLASS_COMPONENT); + return new self(self::CLASS_LEAF); + } + + public static function composite(): self + { + return new self(self::CLASS_COMPOSITE); } public function isPrimitive(): bool @@ -64,11 +70,27 @@ public function isValue(): bool public function isComponent(): bool { - return $this->value === self::CLASS_COMPONENT; + return $this->value === self::CLASS_COMPOSITE + || $this->value === self::CLASS_LEAF; + } + + public function isComposite(): bool + { + return $this->value === self::CLASS_COMPOSITE; + } + + public function isLeaf(): bool + { + return $this->value === self::CLASS_LEAF; } public function getValue(): string { return $this->value; } + + public function __toString(): string + { + return $this->value; + } } diff --git a/Classes/Domain/Component/PropTypeRepository.php b/Classes/Domain/Component/PropTypeRepository.php index c06ea0a..4c4ba06 100755 --- a/Classes/Domain/Component/PropTypeRepository.php +++ b/Classes/Domain/Component/PropTypeRepository.php @@ -15,6 +15,12 @@ */ final class PropTypeRepository implements PropTypeRepositoryInterface { + /** + * @Flow\Inject + * @var ComponentRepository + */ + protected $componentRepository; + public function findByType(?string $packageKey, ?string $componentName, string $type): ?PropType { if (!$this->knowsByType($packageKey, $componentName, $type)) { @@ -52,7 +58,14 @@ public function findPropTypeIdentifier(string $packageKey, string $componentName if ($this->knowsComponent($packageKey, $type)) { $interfaceName = $this->getComponentInterfaceName($packageKey, $type); - return new PropTypeIdentifier($type, $this->getSimpleClassName($interfaceName), $interfaceName, $nullable, PropTypeClass::component()); + $componentType = $this->componentRepository->getComponentType($interfaceName); + return new PropTypeIdentifier( + $type, + $this->getSimpleClassName($interfaceName), + $interfaceName, + $nullable, + $componentType->isLeaf() ? PropTypeClass::leaf() : PropTypeClass::composite() + ); } return null; diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php index 6cc996a..36352ad 100644 --- a/Tests/Unit/Domain/Component/ComponentTest.php +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -24,8 +24,8 @@ public function setUp(): void $propTypeRepository = new DummyPropTypeRepository(); $propTypeRepository->propTypeIdentifiers['Acme.Site']['MySubComponent'] = [ - 'MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', false, PropTypeClass::component()), - '?MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', true, PropTypeClass::component()) + 'MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', false, PropTypeClass::leaf()), + '?MySubComponent' => new PropTypeIdentifier('MySubComponent', 'MySubComponentInterface', 'Acme\Site\Presentation\MySubComponent\MySubComponentInterface', true, PropTypeClass::leaf()) ]; $this->subject = Component::fromInput( @@ -60,11 +60,12 @@ public function testGetInterfaceContent(): void * This file is part of the Acme.Site package. */ +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\ComponentPresentationObjectInterface; use Psr\Http\Message\UriInterface; use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; use Acme\Site\Presentation\MySubComponent\MySubComponentInterface; -interface MyComponentInterface +interface MyComponentInterface extends ComponentPresentationObjectInterface { public function getBool(): bool; @@ -317,7 +318,7 @@ final class MyComponentFactory extends AbstractComponentPresentationObjectFactor public function testGetFusionContent(): void { - Assert::assertSame('prototype(Acme.Site:Component.MyComponent) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + Assert::assertSame('prototype(Acme.Site:Composite.MyComponent) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @presentationObjectInterface = \'Acme\\Site\\Presentation\\MyComponent\\MyComponentInterface\' @styleguide { @@ -342,9 +343,9 @@ public function testGetFusionContent(): void height = 1920 width = 1080 } - subComponent = { + subComponent { } - nullableSubComponent = { + nullableSubComponent { } } } @@ -375,9 +376,9 @@ public function testGetFusionContent(): void
nullableImage:
subComponent:
-
+
nullableSubComponent:
-
+
` } ', From 1f103d574aa3d369ba0d3b043b85f8deb83d6258 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Thu, 30 Apr 2020 10:48:22 +0200 Subject: [PATCH 16/28] Properly render double backslashed class names in fusion --- Classes/Domain/Component/Component.php | 2 +- Tests/Unit/Domain/Component/ComponentTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index 68a8f24..3f3e7a5 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -208,7 +208,7 @@ public function getFusionContent(): string } return 'prototype(' . $this->packageKey . ':' . ucfirst($this->getType()) . '.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { - @presentationObjectInterface = \'' . $this->getNamespace() . '\\' . $this->name . 'Interface\' + @presentationObjectInterface = \'' . str_replace('\\', '\\\\', ucfirst($this->getNamespace())) . '\\\\' . $this->name . 'Interface\' @styleguide { title = \'' . $this->name . '\' diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php index 36352ad..6bf726d 100644 --- a/Tests/Unit/Domain/Component/ComponentTest.php +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -319,7 +319,7 @@ final class MyComponentFactory extends AbstractComponentPresentationObjectFactor public function testGetFusionContent(): void { Assert::assertSame('prototype(Acme.Site:Composite.MyComponent) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { - @presentationObjectInterface = \'Acme\\Site\\Presentation\\MyComponent\\MyComponentInterface\' + @presentationObjectInterface = \'Acme\\\\Site\\\\Presentation\\\\MyComponent\\\\MyComponentInterface\' @styleguide { title = \'MyComponent\' From d5a0a9a277cae2ef654d2441db09f4177c8ccd01 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 11 Mar 2020 11:53:35 +0100 Subject: [PATCH 17/28] Add option to wrap editable properties in divs --- .../Fusion/AbstractComponentPresentationObjectFactory.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php index fe60547..fb117ca 100755 --- a/Classes/Fusion/AbstractComponentPresentationObjectFactory.php +++ b/Classes/Fusion/AbstractComponentPresentationObjectFactory.php @@ -69,15 +69,18 @@ final protected function createWrapper(TraversableNodeInterface $node, Presentat /** * @param TraversableNodeInterface $node * @param string $propertyName + * @param boolean $block * @return string */ - final protected function getEditableProperty(TraversableNodeInterface $node, string $propertyName): string + final protected function getEditableProperty(TraversableNodeInterface $node, string $propertyName, bool $block = false): string { /** @var NodeInterface $node */ return $this->contentElementEditableService->wrapContentProperty( $node, $propertyName, - $node->getProperty($propertyName) ?: '' + ($block ? '
' : '') + . ($node->getProperty($propertyName) ?: '') + . ($block ? '
' : '') ); } From caac9f61fef8d935ef8982873205dbe40eee7867 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 12:23:22 +0200 Subject: [PATCH 18/28] Add proper type annotations for kickstarter-related classes --- .../Command/ComponentCommandController.php | 17 ++- Classes/Domain/Component/Component.php | 83 +++++++++++-- .../Domain/Component/ComponentGenerator.php | 12 ++ .../Domain/Component/ComponentRepository.php | 10 +- Classes/Domain/Component/PropType.php | 46 +++++++ .../Domain/Component/PropTypeRepository.php | 62 +++++++++- Classes/Domain/NoPackageCouldBeResolved.php | 21 ++++ Classes/Domain/PackageResolver.php | 22 +++- Classes/Domain/Value/Value.php | 117 +++++++++++++++--- Classes/Domain/Value/ValueGenerator.php | 8 ++ Classes/Infrastructure/UriService.php | 9 +- 11 files changed, 371 insertions(+), 36 deletions(-) diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index 78dbcda..77875b5 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -28,12 +28,25 @@ class ComponentCommandController extends CommandController */ protected $valueGenerator; - public function kickStartCommand(string $name, string $packageKey = null): void + /** + * @param string $name + * @param null|string $packageKey + * @return void + */ + public function kickStartCommand(string $name, ?string $packageKey = null): void { $this->componentGenerator->generateComponent($name, $this->request->getExceedingArguments(), $packageKey); } - public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = null, string $packageKey = null): void + /** + * @param string $componentName + * @param string $name + * @param string $type + * @param array|string[] $values + * @param null|string $packageKey + * @return void + */ + public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = [], ?string $packageKey = null): void { $this->valueGenerator->generateValue($componentName, $name, $type, $values, $packageKey); } diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index 3f3e7a5..3c5b4f3 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -28,18 +28,25 @@ final class Component */ private $props; - public function __construct(string $packageKey, string $name, array $props, ?PropTypeRepositoryInterface $propTypeRepository = null) + /** + * @param string $packageKey + * @param string $name + * @param array|PropType[] $props + */ + public function __construct(string $packageKey, string $name, array $props) { - foreach ($props as &$propType) { - if (is_string($propType)) { - $propType = $propTypeRepository->findByType($packageKey, $name, $propType); - } - } $this->packageKey = $packageKey; $this->name = $name; $this->props = $props; } + /** + * @param string $packageKey + * @param string $name + * @param array|string[] $serializedProps + * @param PropTypeRepositoryInterface $propTypeRepository + * @return self + */ public static function fromInput(string $packageKey, string $name, array $serializedProps, PropTypeRepositoryInterface $propTypeRepository): self { $props = []; @@ -59,11 +66,17 @@ public static function fromInput(string $packageKey, string $name, array $serial ); } + /** + * @return string + */ public function getPackageKey(): string { return $this->packageKey; } + /** + * @return string + */ public function getName(): string { return $this->name; @@ -72,11 +85,14 @@ public function getName(): string /** * @return array|PropType[] */ - public function getProps() + public function getProps(): array { return $this->props; } + /** + * @return boolean + */ public function isLeaf(): bool { foreach ($this->props as $propType) { @@ -88,6 +104,9 @@ public function isLeaf(): bool return true; } + /** + * @return ComponentType + */ public function getType(): ComponentType { foreach ($this->props as $propType) { @@ -99,36 +118,61 @@ public function getType(): ComponentType return ComponentType::leaf(); } + /** + * @return string + */ public function getFactoryName(): string { return $this->getNamespace() . '\\' . $this->name . 'Factory'; } + /** + * @return string + */ public function getHelperName(): string { return \mb_substr($this->getPackageKey(), \mb_strrpos($this->getPackageKey(), '.') + 1) . '.' . $this->getName(); } + /** + * @param string $packagePath + * @return string + */ public function getInterfacePath(string $packagePath): string { return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . 'Interface.php'; } + /** + * @param string $packagePath + * @return string + */ public function getClassPath(string $packagePath): string { return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . '.php'; } + /** + * @param string $packagePath + * @return string + */ public function getFactoryPath(string $packagePath): string { return $packagePath . 'Classes/Presentation/' . $this->name . '/' . $this->name . 'Factory.php'; } + /** + * @param string $packagePath + * @return string + */ public function getFusionPath(string $packagePath): string { return $packagePath . 'Resources/Private/Fusion/Presentation/' . ucfirst($this->getType()) . '/' . $this->name . '/' . $this->name . '.fusion'; } + /** + * @return string + */ public function getInterfaceContent(): string { return 'getName() . 'Interface extends ComponentPresentationObjectI '; } + /** + * @return string + */ public function getClassContent(): string { return 'getName() . ' extends AbstractComponentPresentationObject '; } + /** + * @return string + */ public function getFactoryContent(): string { return 'getName() . 'Factory extends AbstractComponentPresentatio '; } + /** + * @return string + */ public function getFusionContent(): string { $terms = []; @@ -225,11 +278,17 @@ public function getFusionContent(): string '; } + /** + * @return string + */ private function getNamespace(): string { return \str_replace('.', '\\', $this->packageKey) . '\Presentation\\' . $this->name; } + /** + * @return array|string[] + */ private function getProperties(): array { $properties = []; @@ -243,6 +302,9 @@ private function getProperties(): array return $properties; } + /** + * @return string + */ private function renderUseStatements(): string { $statements = ''; @@ -259,6 +321,9 @@ private function renderUseStatements(): string return $statements; } + /** + * @return string + */ private function renderConstructor(): string { $arguments = []; @@ -274,6 +339,10 @@ private function renderConstructor(): string }'; } + /** + * @param boolean $abstract + * @return array|string[] + */ private function getAccessors(bool $abstract = false): array { $accessors = []; diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index c1dc11b..1f55c9f 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -32,6 +32,13 @@ final class ComponentGenerator */ protected $packageResolver; + /** + * @param string $componentName + * @phpstan-param array $serializedProps + * @param array $serializedProps + * @param string|null $packageKey + * @return void + */ public function generateComponent(string $componentName, array $serializedProps, ?string $packageKey = null): void { $package = $this->packageResolver->resolvePackage($packageKey); @@ -53,6 +60,11 @@ public function generateComponent(string $componentName, array $serializedProps, $this->registerFactory($package, $component); } + /** + * @param FlowPackageInterface $package + * @param Component $component + * @return void + */ private function registerFactory(FlowPackageInterface $package, Component $component): void { $configurationPath = $package->getPackagePath() . 'Configuration/'; diff --git a/Classes/Domain/Component/ComponentRepository.php b/Classes/Domain/Component/ComponentRepository.php index 4f58224..03d2ef2 100755 --- a/Classes/Domain/Component/ComponentRepository.php +++ b/Classes/Domain/Component/ComponentRepository.php @@ -16,12 +16,20 @@ */ final class ComponentRepository { + /** + * @phpstan-param class-string $interfaceName + * @param string $interfaceName + * @return ComponentType + */ public function getComponentType(string $interfaceName): ComponentType { $reflection = new \ReflectionClass($interfaceName); foreach($reflection->getMethods() as $method) { if (\mb_strpos($method->getName(), 'get') === 0 && interface_exists((string) $method->getReturnType())) { - $reflection = new \ReflectionClass((string) $method->getReturnType()); + /** @phpstan-var class-string $getterReturnTypeName */ + $getterReturnTypeName = (string) $method->getReturnType(); + $reflection = new \ReflectionClass($getterReturnTypeName); + if (in_array(ComponentPresentationObjectInterface::class, $reflection->getInterfaceNames())) { return ComponentType::composite(); } diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php index 3c9807e..ad90436 100755 --- a/Classes/Domain/Component/PropType.php +++ b/Classes/Domain/Component/PropType.php @@ -15,6 +15,7 @@ final class PropType { /** + * @phpstan-var array * @var array|string[] */ public static $primitives = [ @@ -24,6 +25,10 @@ final class PropType 'bool' => 'bool' ]; + /** + * @phpstan-var array + * @var array|string[] + */ public static $globalValues = [ 'ImageSource' => ImageSourceHelperInterface::class, 'Uri' => UriInterface::class @@ -54,6 +59,13 @@ final class PropType */ private $class; + /** + * @param string $name + * @param string $simpleName + * @param string $fullyQualifiedName + * @param boolean $nullable + * @param PropTypeClass $class + */ private function __construct(string $name, string $simpleName, string $fullyQualifiedName, bool $nullable, PropTypeClass $class) { $this->name = $name; @@ -63,6 +75,13 @@ private function __construct(string $name, string $simpleName, string $fullyQual $this->class = $class; } + /** + * @param string $packageKey + * @param string $componentName + * @param string $type + * @param PropTypeRepositoryInterface $propTypeRepository + * @return self + */ public static function create(string $packageKey, string $componentName, string $type, PropTypeRepositoryInterface $propTypeRepository): self { if (!$identity = $propTypeRepository->findPropTypeIdentifier($packageKey, $componentName, $type)) { @@ -78,46 +97,73 @@ public static function create(string $packageKey, string $componentName, string ); } + /** + * @return string + */ public function getName(): string { return $this->name; } + /** + * @return string + */ public function getSimpleName(): string { return $this->simpleName; } + /** + * @return string + */ public function getFullyQualifiedName(): string { return $this->fullyQualifiedName; } + /** + * @return boolean + */ public function isNullable(): bool { return $this->nullable; } + /** + * @return PropTypeClass + */ public function getClass(): PropTypeClass { return $this->class; } + /** + * @return string + */ public function toUse(): string { return $this->fullyQualifiedName; } + /** + * @return string + */ public function toType(): string { return ($this->isNullable() ? '?' : '') . $this->simpleName; } + /** + * @return string + */ public function toVar(): string { return $this->simpleName . ($this->isNullable() ? '|null' : ''); } + /** + * @return string + */ public function toStyleGuidePropValue(): string { $styleGuideValue = ''; diff --git a/Classes/Domain/Component/PropTypeRepository.php b/Classes/Domain/Component/PropTypeRepository.php index 4c4ba06..5928f27 100755 --- a/Classes/Domain/Component/PropTypeRepository.php +++ b/Classes/Domain/Component/PropTypeRepository.php @@ -21,8 +21,18 @@ final class PropTypeRepository implements PropTypeRepositoryInterface */ protected $componentRepository; + /** + * @param null|string $packageKey + * @param null|string $componentName + * @param string $type + * @return null|PropType + */ public function findByType(?string $packageKey, ?string $componentName, string $type): ?PropType { + if ($packageKey === null || $componentName === null) { + return null; + } + if (!$this->knowsByType($packageKey, $componentName, $type)) { return null; } @@ -30,6 +40,12 @@ public function findByType(?string $packageKey, ?string $componentName, string $ return PropType::create($packageKey, $componentName, $type, $this); } + /** + * @param string $packageKey + * @param string $componentName + * @param string $type + * @return null|PropTypeIdentifier + */ public function findPropTypeIdentifier(string $packageKey, string $componentName, string $type): ?PropTypeIdentifier { if (!$this->knowsByType($packageKey, $componentName, $type)) { @@ -71,11 +87,21 @@ public function findPropTypeIdentifier(string $packageKey, string $componentName return null; } + /** + * @param string $className + * @return string + */ private function getSimpleClassName(string $className): string { return \mb_substr($className, \mb_strrpos($className, '\\') + 1); } + /** + * @param string $packageKey + * @param string $componentName + * @param string $type + * @return boolean + */ public function knowsByType(string $packageKey, string $componentName, string $type): bool { $type = trim($type, '?'); @@ -86,35 +112,69 @@ public function knowsByType(string $packageKey, string $componentName, string $t || $this->knowsComponent($packageKey, $type); } + /** + * @param string $type + * @return boolean + */ private function knowsPrimitive(string $type): bool { return isset(PropType::$primitives[$type]); } + /** + * @param string $type + * @return boolean + */ private function knowsGlobalValue(string $type): bool { return isset(PropType::$globalValues[$type]); } + /** + * @param string $packageKey + * @param string $componentName + * @param string $type + * @return boolean + */ private function knowsValue(string $packageKey, string $componentName, string $type): bool { return class_exists($this->getValueClassName($packageKey, $componentName, $type)); } + /** + * @param string $packageKey + * @param string $componentName + * @param string $type + * @return string + */ private function getValueClassName(string $packageKey, string $componentName, string $type): string { return \str_replace('.', '\\', $packageKey) . '\Presentation\\' . $componentName . '\\' . $type; } + /** + * @param string $packageKey + * @param string $type + * @return boolean + */ private function knowsComponent(string $packageKey, string $type): bool { return interface_exists($this->getComponentInterfaceName($packageKey, $type)); } + /** + * @param string $packageKey + * @param string $type + * @phpstan-return class-string + * @return string + */ private function getComponentInterfaceName(string $packageKey, string $type): string { - return \str_replace('.', '\\', $packageKey) + /** @phpstan-var class-string $interfaceName */ + $interfaceName = \str_replace('.', '\\', $packageKey) . '\Presentation\\' . $type . '\\' . $type . 'Interface'; + + return $interfaceName; } } diff --git a/Classes/Domain/NoPackageCouldBeResolved.php b/Classes/Domain/NoPackageCouldBeResolved.php index 7442a19..c6b3c13 100755 --- a/Classes/Domain/NoPackageCouldBeResolved.php +++ b/Classes/Domain/NoPackageCouldBeResolved.php @@ -15,8 +15,29 @@ */ class NoPackageCouldBeResolved extends \InvalidArgumentException { + /** + * @return self + */ public static function becauseNoneIsConfiguredAndNoSitePackageIsAvailable(): self { return new self('No package could be resolved for component generation. Please specify a package key, configure a default or create a site package', 1582673201); } + + /** + * @param string $packageKey + * @return self + */ + public static function becauseGivenPackageKeyDoesNotReferToAFlowPackage(string $packageKey): self + { + return new self('No package could be resolved for component generation, because the given package key "' . $packageKey . '" does not refer to a flow package.', 1582673202); + } + + /** + * @param string $packageKey + * @return self + */ + public static function becauseDefaultPackageKeyDoesNotReferToAFlowPackage(string $packageKey): self + { + return new self('No package could be resolved for component generation, because the default package key "' . $packageKey . '" does not refer to a flow package.', 1582673202); + } } diff --git a/Classes/Domain/PackageResolver.php b/Classes/Domain/PackageResolver.php index b95a7dc..ab13447 100755 --- a/Classes/Domain/PackageResolver.php +++ b/Classes/Domain/PackageResolver.php @@ -8,7 +8,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Package\FlowPackageInterface; -use Neos\Flow\Package\PackageInterface; use Neos\Flow\Package\PackageManager; /** @@ -33,19 +32,32 @@ final class PackageResolver public function resolvePackage(?string $packageKey = null): FlowPackageInterface { if ($packageKey) { - return $this->packageManager->getPackage($packageKey); + $package = $this->packageManager->getPackage($packageKey); + if ($package instanceof FlowPackageInterface) { + return $package; + } else { + throw NoPackageCouldBeResolved:: + becauseGivenPackageKeyDoesNotReferToAFlowPackage($packageKey); + } } if ($this->defaultPackageKey) { - return $this->packageManager->getPackage($this->defaultPackageKey); + $package = $this->packageManager->getPackage($this->defaultPackageKey); + if ($package instanceof FlowPackageInterface) { + return $package; + } else { + throw NoPackageCouldBeResolved:: + becauseDefaultPackageKeyDoesNotReferToAFlowPackage($this->defaultPackageKey); + } } foreach ($this->packageManager->getAvailablePackages() as $availablePackage) { - /** @var PackageInterface $availablePackage */ + /** @var FlowPackageInterface $availablePackage */ if ($availablePackage->getComposerManifest('type') === 'neos-site') { return $availablePackage; } } - throw NoPackageCouldBeResolved::becauseNoneIsConfiguredAndNoSitePackageIsAvailable(); + throw NoPackageCouldBeResolved:: + becauseNoneIsConfiguredAndNoSitePackageIsAvailable(); } } diff --git a/Classes/Domain/Value/Value.php b/Classes/Domain/Value/Value.php index b73f5b8..4456796 100755 --- a/Classes/Domain/Value/Value.php +++ b/Classes/Domain/Value/Value.php @@ -33,10 +33,17 @@ final class Value private $type; /** - * @var array|null + * @var null|string[] */ private $values; + /** + * @param string $packageKey + * @param string $componentName + * @param string $name + * @param string $type + * @param null|string[] $values + */ public function __construct(string $packageKey, string $componentName, string $name, string $type, ?array $values) { $this->packageKey = $packageKey; @@ -49,36 +56,58 @@ public function __construct(string $packageKey, string $componentName, string $n $this->values = $values; } + /** + * @return string + */ public function getPackageKey(): string { return $this->packageKey; } + /** + * @return string + */ public function getComponentName(): string { return $this->componentName; } + /** + * @return string + */ public function getName(): string { return $this->name; } + /** + * @return string + */ public function getType(): string { return $this->type; } + /** + * @return null|string[] + */ public function getValues(): ?array { return $this->values; } + /** + * @param string $packagePath + * @return string + */ public function getClassPath(string $packagePath): string { return $packagePath . 'Classes/Presentation/' . $this->componentName . '/' . $this->name . '.php'; } + /** + * @return string + */ public function getClassContent(): string { $variable = '$' . $this->type; @@ -144,11 +173,18 @@ public function __toString(): string '; } + /** + * @param string $packagePath + * @return string + */ public function getExceptionPath(string $packagePath): string { return $packagePath . 'Classes/Presentation/' . $this->componentName . '/' . $this->name . 'IsInvalid.php'; } + /** + * @return string + */ public function getExceptionContent(): string { return 'type . '; } + /** + * @param string $packagePath + * @return string + */ public function getProviderPath(string $packagePath): string { return $packagePath . 'Classes/Application/' . $this->name . 'Provider.php'; } + /** + * @return string + */ public function getProviderContent(): string { $arrayName = lcfirst($this->getPluralName()); @@ -234,6 +277,9 @@ public function allowsCallOfMethod($methodName): bool '; } + /** + * @return string + */ private function getPluralName(): string { return \mb_substr($this->name, -1) === 'y' @@ -241,69 +287,101 @@ private function getPluralName(): string : $this->name . 's'; } + /** + * @return string + */ private function getDataSourceIdentifier(): string { return strtolower(str_replace('.', '-', $this->packageKey) . '-' . implode('-', $this->splitName(true))); } + /** + * @return string + */ private function getDataSourceNamespace(): string { return \str_replace('.', '\\', $this->packageKey) . '\Application'; } + /** + * @return string + */ private function getNamespace(): string { return \str_replace('.', '\\', $this->packageKey) . '\Presentation\\' . $this->componentName; } + /** + * @return string + */ private function renderConstants(): string { $constants = []; - foreach ($this->values as $value) { - $constants[] = 'const ' . $this->getConstantName($value) . ' = \'' . $value . '\';'; + if (is_array($this->values)) { + foreach ($this->values as $value) { + $constants[] = 'const ' . $this->getConstantName($value) . ' = \'' . $value . '\';'; + } } return trim(implode("\n ", $constants)); } + /** + * @return string + */ private function renderNamedConstructors(): string { $constructors = []; - foreach ($this->values as $value) { - - $constructors[] = 'public static function ' . $value . '(): self + if (is_array($this->values)) { + foreach ($this->values as $value) { + $constructors[] = 'public static function ' . $value . '(): self { return new self(self::' . $this->getConstantName($value) . '); }'; + } } return trim(implode("\n\n ", $constructors)); } + /** + * @return string + */ private function renderComparators(): string { $comparators = []; - foreach ($this->values as $value) { - - $comparators[] = 'public function getIs' . ucfirst($value) . '(): bool + if (is_array($this->values)) { + foreach ($this->values as $value) { + $comparators[] = 'public function getIs' . ucfirst($value) . '(): bool { return $this->value === self::' . $this->getConstantName($value) . '; }'; + } } return trim(implode("\n\n ", $comparators)); } + /** + * @return string + */ public function renderValues(): string { $values = []; - foreach ($this->values as $value) { - $values[] = 'self::' . $this->getConstantName($value) . ','; + + if (is_array($this->values)) { + foreach ($this->values as $value) { + $values[] = 'self::' . $this->getConstantName($value) . ','; + } } return trim(trim(implode("\n ", $values)), ','); } + /** + * @param string $value + * @return string + */ private function getConstantName(string $value): string { $parts = $this->splitName(); @@ -314,16 +392,23 @@ private function getConstantName(string $value): string return 'VALUE_' . strtoupper($value); } + /** + * @param boolean $plural + * @return string[] + */ private function splitName(bool $plural = false): array { $name = $plural ? $this->getPluralName() : $this->name; $nameParts = []; $parts = preg_split("/([A-Z])/", $name, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); - foreach ($parts as $i => $part) { - if ($i % 2 === 0) { - $nameParts[$i / 2] = $part; - } else { - $nameParts[($i - 1) / 2] .= $part; + + if (is_array($parts)) { + foreach ($parts as $i => $part) { + if ($i % 2 === 0) { + $nameParts[$i / 2] = $part; + } else { + $nameParts[($i - 1) / 2] .= $part; + } } } diff --git a/Classes/Domain/Value/ValueGenerator.php b/Classes/Domain/Value/ValueGenerator.php index c20d436..6e1e2f0 100755 --- a/Classes/Domain/Value/ValueGenerator.php +++ b/Classes/Domain/Value/ValueGenerator.php @@ -23,6 +23,14 @@ final class ValueGenerator */ protected $packageResolver; + /** + * @param string $componentName + * @param string $name + * @param string $type + * @param array|string[] $values + * @param null|string $packageKey + * @return void + */ public function generateValue(string $componentName, string $name, string $type, array $values, ?string $packageKey = null): void { $package = $this->packageResolver->resolvePackage($packageKey); diff --git a/Classes/Infrastructure/UriService.php b/Classes/Infrastructure/UriService.php index 2dc9390..895d434 100644 --- a/Classes/Infrastructure/UriService.php +++ b/Classes/Infrastructure/UriService.php @@ -5,7 +5,6 @@ * This file is part of the PackageFactory.AtomicFusion.PresentationObjects package. */ -use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface; use Neos\ContentRepository\Domain\Service\Context as ContentContext; use Neos\Flow\Annotations as Flow; @@ -113,15 +112,17 @@ public function getControllerContext(): ControllerContext $requestHandler = $this->bootstrap->getActiveRequestHandler(); if ($requestHandler instanceof Http\RequestHandler) { $request = $requestHandler->getHttpRequest(); + $response = $requestHandler->getHttpResponse(); } else { - $request = ServerRequest::fromGlobals(); + $request = Http\Request::createFromEnvironment(); + $response = new Http\Response(); } - $actionRequest = Mvc\ActionRequest::fromHttpRequest($request); + $actionRequest = new Mvc\ActionRequest($request); $uriBuilder = new Mvc\Routing\UriBuilder(); $uriBuilder->setRequest($actionRequest); $this->controllerContext = new Mvc\Controller\ControllerContext( $actionRequest, - new Mvc\ActionResponse(), + $response, new Mvc\Controller\Arguments(), $uriBuilder ); From 67e17ab9cd5b00de663dae319bc2b984624dbf33 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 12:43:20 +0200 Subject: [PATCH 19/28] Add basic command documentation for ComponentCommandController --- .../Command/ComponentCommandController.php | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index 77875b5..07d83a7 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -12,7 +12,7 @@ use PackageFactory\AtomicFusion\PresentationObjects\Domain\Value\ValueGenerator; /** - * The command controller for component handling + * The command controller for kickstarting PresentationObject components */ class ComponentCommandController extends CommandController { @@ -29,8 +29,10 @@ class ComponentCommandController extends CommandController protected $valueGenerator; /** - * @param string $name - * @param null|string $packageKey + * Create a new PresentationObject component and factory + * + * @param string $name The name of the new component + * @param null|string $packageKey Package key of an optional target package, if not set the configured default package or the first available site package will be used * @return void */ public function kickStartCommand(string $name, ?string $packageKey = null): void @@ -39,11 +41,13 @@ public function kickStartCommand(string $name, ?string $packageKey = null): void } /** - * @param string $componentName - * @param string $name - * @param string $type - * @param array|string[] $values - * @param null|string $packageKey + * Create a new pseudo-enum value object + * + * @param string $componentName The name of the component the new pseudo-enum belongs to + * @param string $name The name of the new pseudo-enum + * @param string $type The type of the new pseudo-enum (must be one of: "string", "int") + * @param array|string[] $values A comma-separated list of values for the new pseudo-enum + * @param null|string $packageKey Package key of an optional target package, if not set the configured default package or the first available site package will be used * @return void */ public function kickStartValueCommand(string $componentName, string $name, string $type, array $values = [], ?string $packageKey = null): void From 3d2ea0ed60cdcee4c1d94ad2bc6f231b2c4165c8 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 15:57:44 +0200 Subject: [PATCH 20/28] Add snapshot tests for kickstarter --- .../Domain/Component/ComponentGenerator.php | 4 +- Classes/Domain/Component/PropType.php | 2 +- Classes/Domain/PackageResolver.php | 2 +- Classes/Domain/PackageResolverInterface.php | 17 ++ Classes/Domain/Value/Value.php | 5 +- Classes/Domain/Value/ValueGenerator.php | 19 +- ...sentationObjectComponentImplementation.php | 3 +- .../Component/ComponentGeneratorTest.php | 223 ++++++++++++++++++ Tests/Unit/Domain/Component/PropTypeTest.php | 97 ++++++++ ...eratesComponents with data set card__1.txt | 58 +++++ ...eratesComponents with data set card__2.txt | 20 ++ ...eratesComponents with data set card__3.txt | 12 + ...eratesComponents with data set card__4.txt | 4 + ...eratesComponents with data set card__5.txt | 25 ++ ...esComponents with data set headline__1.txt | 57 +++++ ...esComponents with data set headline__2.txt | 19 ++ ...esComponents with data set headline__3.txt | 12 + ...esComponents with data set headline__4.txt | 4 + ...esComponents with data set headline__5.txt | 22 ++ ...ratesComponents with data set image__1.txt | 56 +++++ ...ratesComponents with data set image__2.txt | 18 ++ ...ratesComponents with data set image__3.txt | 12 + ...ratesComponents with data set image__4.txt | 4 + ...ratesComponents with data set image__5.txt | 25 ++ ...eratesComponents with data set link__1.txt | 44 ++++ ...eratesComponents with data set link__2.txt | 16 ++ ...eratesComponents with data set link__3.txt | 12 + ...eratesComponents with data set link__4.txt | 4 + ...eratesComponents with data set link__5.txt | 19 ++ ...th data set text in default package__1.txt | 31 +++ ...th data set text in default package__2.txt | 13 + ...th data set text in default package__3.txt | 12 + ...th data set text in default package__4.txt | 5 + ...th data set text in default package__5.txt | 16 ++ ...eratesComponents with data set text__1.txt | 31 +++ ...eratesComponents with data set text__2.txt | 13 + ...eratesComponents with data set text__3.txt | 12 + ...eratesComponents with data set text__4.txt | 4 + ...eratesComponents with data set text__5.txt | 16 ++ .../Unit/Domain/Value/ValueGeneratorTest.php | 142 +++++++++++ Tests/Unit/Domain/Value/ValueTest.php | 4 +- ...esValues with data set headlinetype__1.txt | 89 +++++++ ...esValues with data set headlinetype__2.txt | 19 ++ ...esValues with data set headlinetype__3.txt | 50 ++++ ...esValues with data set trafficlight__1.txt | 84 +++++++ ...esValues with data set trafficlight__2.txt | 19 ++ ...esValues with data set trafficlight__3.txt | 50 ++++ ...ComponentPresentationObjectFactoryTest.php | 30 ++- composer.json | 3 +- phpunit.xml | 2 +- 50 files changed, 1441 insertions(+), 19 deletions(-) create mode 100644 Classes/Domain/PackageResolverInterface.php create mode 100644 Tests/Unit/Domain/Component/ComponentGeneratorTest.php create mode 100644 Tests/Unit/Domain/Component/PropTypeTest.php create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__5.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__5.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__5.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__5.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__5.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__1.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__2.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__3.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__4.txt create mode 100644 Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__5.txt create mode 100644 Tests/Unit/Domain/Value/ValueGeneratorTest.php create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__1.txt create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__2.txt create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__3.txt create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__1.txt create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__2.txt create mode 100644 Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__3.txt diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 1f55c9f..21fb777 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -9,7 +9,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Files; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolver; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolverInterface; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Dumper as YamlWriter; @@ -28,7 +28,7 @@ final class ComponentGenerator /** * @Flow\Inject - * @var PackageResolver + * @var PackageResolverInterface */ protected $packageResolver; diff --git a/Classes/Domain/Component/PropType.php b/Classes/Domain/Component/PropType.php index ad90436..97fa2c7 100755 --- a/Classes/Domain/Component/PropType.php +++ b/Classes/Domain/Component/PropType.php @@ -66,7 +66,7 @@ final class PropType * @param boolean $nullable * @param PropTypeClass $class */ - private function __construct(string $name, string $simpleName, string $fullyQualifiedName, bool $nullable, PropTypeClass $class) + public function __construct(string $name, string $simpleName, string $fullyQualifiedName, bool $nullable, PropTypeClass $class) { $this->name = $name; $this->simpleName = $simpleName; diff --git a/Classes/Domain/PackageResolver.php b/Classes/Domain/PackageResolver.php index ab13447..214a6aa 100755 --- a/Classes/Domain/PackageResolver.php +++ b/Classes/Domain/PackageResolver.php @@ -15,7 +15,7 @@ * * @Flow\Scope("singleton") */ -final class PackageResolver +final class PackageResolver implements PackageResolverInterface { /** * @Flow\Inject diff --git a/Classes/Domain/PackageResolverInterface.php b/Classes/Domain/PackageResolverInterface.php new file mode 100644 index 0000000..da914af --- /dev/null +++ b/Classes/Domain/PackageResolverInterface.php @@ -0,0 +1,17 @@ +getNamespace() . '; @@ -203,7 +204,7 @@ final class ' . $this->getName() . 'IsInvalid extends \DomainException { public static function becauseItMustBeOneOfTheDefinedConstants(' . $this->type . ' $attemptedValue): self { - return new self(\'The given value "\' . $attemptedValue . \'" is no valid ' . $this->name . ', must be one of the defined constants. \', ' . time() . '); + return new self(\'The given value "\' . $attemptedValue . \'" is no valid ' . $this->name . ', must be one of the defined constants. \', ' . $now->getTimestamp() . '); } } '; diff --git a/Classes/Domain/Value/ValueGenerator.php b/Classes/Domain/Value/ValueGenerator.php index 6e1e2f0..ead0d0d 100755 --- a/Classes/Domain/Value/ValueGenerator.php +++ b/Classes/Domain/Value/ValueGenerator.php @@ -8,7 +8,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Utility\Files; -use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolver; +use PackageFactory\AtomicFusion\PresentationObjects\Domain\PackageResolverInterface; /** * The value generator domain service @@ -19,10 +19,23 @@ final class ValueGenerator { /** * @Flow\Inject - * @var PackageResolver + * @var PackageResolverInterface */ protected $packageResolver; + /** + * @var \DateTimeImmutable + */ + protected $now; + + /** + * @param null|\DateTimeImmutable $now + */ + public function __construct(?\DateTimeImmutable $now = null) + { + $this->now = $now ?? new \DateTimeImmutable(); + } + /** * @param string $componentName * @param string $name @@ -42,7 +55,7 @@ public function generateValue(string $componentName, string $name, string $type, Files::createDirectoryRecursively($classPath); } file_put_contents($value->getClassPath($packagePath), $value->getClassContent()); - file_put_contents($value->getExceptionPath($packagePath), $value->getExceptionContent()); + file_put_contents($value->getExceptionPath($packagePath), $value->getExceptionContent($this->now)); $dataSourcePath = $packagePath . 'Classes/Application/'; if (!is_dir($dataSourcePath)) { diff --git a/Classes/Fusion/PresentationObjectComponentImplementation.php b/Classes/Fusion/PresentationObjectComponentImplementation.php index a196d4c..7703e02 100755 --- a/Classes/Fusion/PresentationObjectComponentImplementation.php +++ b/Classes/Fusion/PresentationObjectComponentImplementation.php @@ -92,10 +92,10 @@ protected function getPresentationObject(): ComponentPresentationObjectInterface * @TODO: We need to have a look at this one, it doesn't seem to be used anywhere (@WBE) * * @return string + * @codeCoverageIgnore */ public function getContentElementFusionPath(): string { - // @codeCoverageIgnoreStart $fusionPathSegments = explode('/', $this->path); $numberOfFusionPathSegments = count($fusionPathSegments); if (isset($fusionPathSegments[$numberOfFusionPathSegments - 3]) @@ -114,6 +114,5 @@ public function getContentElementFusionPath(): string return implode('/', array_slice($fusionPathSegments, 0, -4)); } return $this->path; - // @codeCoverageIgnoreEnd } } diff --git a/Tests/Unit/Domain/Component/ComponentGeneratorTest.php b/Tests/Unit/Domain/Component/ComponentGeneratorTest.php new file mode 100644 index 0000000..00d01bc --- /dev/null +++ b/Tests/Unit/Domain/Component/ComponentGeneratorTest.php @@ -0,0 +1,223 @@ + + */ + protected $propTypeRepository; + + /** + * @var ObjectProphecy + */ + protected $sitePackage; + + /** + * @var ObjectProphecy + */ + protected $defaultPackage; + + /** + * @var ObjectProphecy + */ + protected $packageResolver; + + /** + * @var ComponentGenerator + */ + protected $componentGenerator; + + /** + * @before + * @return void + */ + public function setUpComponentGeneratorTest(): void + { + vfsStream::setup('DistributionPackages', null, [ + 'Vendor.Site' => [], + 'Vendor.Default' => [ + 'Configuration' => [ + 'Settings.PresentationHelpers.yaml' => join(PHP_EOL, [ + 'Neos:', + ' Fusion:', + ' defaultContext:', + ' Existing.Helper: Some\OtherPackage\Existing\HelperFactory', + ]) + ] + ], + ]); + + $this->prophet = new Prophet(); + + $this->propTypeRepository = $this->prophet->prophesize(PropTypeRepositoryInterface::class); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), 'string') + ->willReturn(new PropType('string', 'string', 'string', false, PropTypeClass::primitive())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), '?string') + ->willReturn(new PropType('string', 'string', 'string', true, PropTypeClass::primitive())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), 'HeadlineType') + ->willReturn(new PropType('HeadlineType', 'HeadlineType', 'HeadlineType', false, PropTypeClass::value())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), 'HeadlineLook') + ->willReturn(new PropType('HeadlineLook', 'HeadlineLook', 'HeadlineLook', false, PropTypeClass::value())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), 'ImageSourceHelperInterface') + ->willReturn(new PropType('ImageSourceHelperInterface', 'ImageSourceHelperInterface', 'ImageSourceHelperInterface', false, PropTypeClass::globalValue())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), 'UriInterface') + ->willReturn(new PropType('UriInterface', 'UriInterface', 'UriInterface', false, PropTypeClass::globalValue())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), '?Image') + ->willReturn(new PropType('Image', 'Image', 'Image', true, PropTypeClass::leaf())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), '?Text') + ->willReturn(new PropType('Text', 'Text', 'Text', true, PropTypeClass::leaf())); + $this->propTypeRepository + ->findByType(Argument::any(), Argument::any(), '?Link') + ->willReturn(new PropType('Link', 'Link', 'Link', true, PropTypeClass::leaf())); + + $this->sitePackage = $this->prophet->prophesize(FlowPackageInterface::class); + $this->sitePackage + ->getPackageKey() + ->willReturn('Vendor.Site'); + $this->sitePackage + ->getPackagePath() + ->willReturn('vfs://DistributionPackages/Vendor.Site/'); + + $this->defaultPackage = $this->prophet->prophesize(FlowPackageInterface::class); + $this->defaultPackage + ->getPackageKey() + ->willReturn('Vendor.Default'); + $this->defaultPackage + ->getPackagePath() + ->willReturn('vfs://DistributionPackages/Vendor.Default/'); + + $this->packageResolver = $this->prophet->prophesize(PackageResolverInterface::class); + $this->packageResolver + ->resolvePackage('Vendor.Site') + ->willReturn($this->sitePackage); + $this->packageResolver + ->resolvePackage(null) + ->willReturn($this->defaultPackage); + + $this->componentGenerator = new ComponentGenerator(); + + $this->inject($this->componentGenerator, 'propTypeRepository', $this->propTypeRepository->reveal()); + $this->inject($this->componentGenerator, 'packageResolver', $this->packageResolver->reveal()); + } + + /** + * @after + * @return void + */ + public function tearDownComponentGeneratorTest(): void + { + $this->prophet->checkPredictions(); + } + + /** + * @return array + */ + public function exampleProvider(): array + { + return [ + 'text' => + ['Text', ['content:string'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Text/Text.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Text/TextInterface.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Text/TextFactory.php', + 'vfs://DistributionPackages/Vendor.Site/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Site/Resources/Private/Fusion/Presentation/Leaf/Text/Text.fusion' + ]], + 'text in default package' => + ['Text', ['content:string'], null, [ + 'vfs://DistributionPackages/Vendor.Default/Classes/Presentation/Text/Text.php', + 'vfs://DistributionPackages/Vendor.Default/Classes/Presentation/Text/TextInterface.php', + 'vfs://DistributionPackages/Vendor.Default/Classes/Presentation/Text/TextFactory.php', + 'vfs://DistributionPackages/Vendor.Default/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Default/Resources/Private/Fusion/Presentation/Leaf/Text/Text.fusion' + ]], + 'headline' => + ['Headline', ['type:HeadlineType', 'look:HeadlineLook', 'content:string'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Headline/Headline.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Headline/HeadlineInterface.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Headline/HeadlineFactory.php', + 'vfs://DistributionPackages/Vendor.Site/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Site/Resources/Private/Fusion/Presentation/Leaf/Headline/Headline.fusion' + ]], + 'image' => + ['Image', ['src:ImageSourceHelperInterface', 'alt:string', 'title:?string'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Image/Image.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Image/ImageInterface.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Image/ImageFactory.php', + 'vfs://DistributionPackages/Vendor.Site/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Site/Resources/Private/Fusion/Presentation/Leaf/Image/Image.fusion' + ]], + 'link' => + ['Link', ['href:UriInterface', 'title:?string'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Link/Link.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Link/LinkInterface.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Link/LinkFactory.php', + 'vfs://DistributionPackages/Vendor.Site/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Site/Resources/Private/Fusion/Presentation/Leaf/Link/Link.fusion' + ]], + 'card' => + ['Card', ['image:?Image', 'text:?Text', 'link:?Link'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Card/Card.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Card/CardInterface.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Card/CardFactory.php', + 'vfs://DistributionPackages/Vendor.Site/Configuration/Settings.PresentationHelpers.yaml', + 'vfs://DistributionPackages/Vendor.Site/Resources/Private/Fusion/Presentation/Composite/Card/Card.fusion' + ]], + ]; + } + + /** + * @test + * @dataProvider exampleProvider + * @param string $componentName + * @param string[] $serializedProps + * @param null|string $packageKey + * @param string[] $expectedFileNames + * @return void + */ + public function generatesComponents(string $componentName, array $serializedProps, ?string $packageKey, array $expectedFileNames): void + { + $this->componentGenerator->generateComponent($componentName, $serializedProps, $packageKey); + + foreach ($expectedFileNames as $fileName) { + $this->assertFileExists($fileName); + $this->assertMatchesSnapshot(file_get_contents($fileName)); + } + } +} diff --git a/Tests/Unit/Domain/Component/PropTypeTest.php b/Tests/Unit/Domain/Component/PropTypeTest.php new file mode 100644 index 0000000..74885df --- /dev/null +++ b/Tests/Unit/Domain/Component/PropTypeTest.php @@ -0,0 +1,97 @@ + + */ + private $propTypeRepository; + + /** + * @before + * @return void + */ + public function setUpPropTypeTest(): void + { + $this->prophet = new Prophet(); + + $this->propTypeRepository = $this->prophet->prophesize(PropTypeRepositoryInterface::class); + $this->propTypeRepository + ->findPropTypeIdentifier('Vendor.Site', 'TestComponent', '?string') + ->willReturn(new PropTypeIdentifier('string', 'string', 'string', true, PropTypeClass::primitive())); + $this->propTypeRepository + ->findPropTypeIdentifier('Vendor.Site', 'TestComponent', 'Some\\UnknownClass') + ->willReturn(null); + } + + /** + * @after + * @return void + */ + public function tearDownPropTypeTest(): void + { + $this->prophet->checkPredictions(); + } + + /** + * @test + * @return void + */ + public function canBeCreatedFromKnownTypeReference(): void + { + $propType = PropType::create( + 'Vendor.Site', + 'TestComponent', + '?string', + $this->propTypeRepository->reveal() + ); + + $this->assertEquals('string', $propType->getName()); + $this->assertEquals('string', $propType->getSimpleName()); + $this->assertEquals('string', $propType->getFullyQualifiedName()); + $this->assertEquals(true, $propType->isNullable()); + $this->assertEquals(true, $propType->getClass()->isPrimitive()); + $this->assertEquals('string', $propType->toUse()); + $this->assertEquals('?string', $propType->toType()); + $this->assertEquals('string|null', $propType->toVar()); + $this->assertEquals('= \'Text\'', $propType->toStyleGuidePropValue()); + } + + /** + * @test + * @return void + */ + public function cannotBeCreatedFromUnknownTypeReference(): void + { + $this->expectException(PropTypeIsInvalid::class); + + PropType::create( + 'Vendor.Site', + 'TestComponent', + 'Some\\UnknownClass', + $this->propTypeRepository->reveal() + ); + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__1.txt new file mode 100644 index 0000000..58b1a63 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__1.txt @@ -0,0 +1,58 @@ +image = $image; + $this->text = $text; + $this->link = $link; + } + + public function getImage(): ?Image + { + return $this->image; + } + + public function getText(): ?Text + { + return $this->text; + } + + public function getLink(): ?Link + { + return $this->link; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__2.txt new file mode 100644 index 0000000..cd234ce --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set card__2.txt @@ -0,0 +1,20 @@ + +
image:
+
+
text:
+
+
link:
+
+ ` +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__1.txt new file mode 100644 index 0000000..2574353 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__1.txt @@ -0,0 +1,57 @@ +type = $type; + $this->look = $look; + $this->content = $content; + } + + public function getType(): HeadlineType + { + return $this->type; + } + + public function getLook(): HeadlineLook + { + return $this->look; + } + + public function getContent(): string + { + return $this->content; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__2.txt new file mode 100644 index 0000000..33cb469 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set headline__2.txt @@ -0,0 +1,19 @@ + +
type:
+
{presentationObject.type}
+
look:
+
{presentationObject.look}
+
content:
+
{presentationObject.content}
+ ` +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__1.txt new file mode 100644 index 0000000..6f96d0d --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__1.txt @@ -0,0 +1,56 @@ +src = $src; + $this->alt = $alt; + $this->title = $title; + } + + public function getSrc(): ImageSourceHelperInterface + { + return $this->src; + } + + public function getAlt(): string + { + return $this->alt; + } + + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__2.txt new file mode 100644 index 0000000..30ca6bb --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set image__2.txt @@ -0,0 +1,18 @@ + +
src:
+
{presentationObject.src}
+
alt:
+
{presentationObject.alt}
+
title:
+
{presentationObject.title}
+ ` +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__1.txt new file mode 100644 index 0000000..9b72b1e --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__1.txt @@ -0,0 +1,44 @@ +href = $href; + $this->title = $title; + } + + public function getHref(): UriInterface + { + return $this->href; + } + + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__2.txt new file mode 100644 index 0000000..a1e3970 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set link__2.txt @@ -0,0 +1,16 @@ + +
href:
+
{presentationObject.href}
+
title:
+
{presentationObject.title}
+ ` +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__1.txt new file mode 100644 index 0000000..b5e8317 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__1.txt @@ -0,0 +1,31 @@ +content = $content; + } + + public function getContent(): string + { + return $this->content; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__2.txt new file mode 100644 index 0000000..562c84a --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text in default package__2.txt @@ -0,0 +1,13 @@ + +
content:
+
{presentationObject.content}
+ ` +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__1.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__1.txt new file mode 100644 index 0000000..cf3775a --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__1.txt @@ -0,0 +1,31 @@ +content = $content; + } + + public function getContent(): string + { + return $this->content; + } +} diff --git a/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__2.txt b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__2.txt new file mode 100644 index 0000000..9034c38 --- /dev/null +++ b/Tests/Unit/Domain/Component/__snapshots__/ComponentGeneratorTest__generatesComponents with data set text__2.txt @@ -0,0 +1,13 @@ + +
content:
+
{presentationObject.content}
+ ` +} diff --git a/Tests/Unit/Domain/Value/ValueGeneratorTest.php b/Tests/Unit/Domain/Value/ValueGeneratorTest.php new file mode 100644 index 0000000..4998c84 --- /dev/null +++ b/Tests/Unit/Domain/Value/ValueGeneratorTest.php @@ -0,0 +1,142 @@ + + */ + protected $sitePackage; + + /** + * @var ObjectProphecy + */ + protected $defaultPackage; + + /** + * @var ObjectProphecy + */ + protected $packageResolver; + + /** + * @var ValueGenerator + */ + protected $valueGenerator; + + /** + * @before + * @return void + */ + public function setUpComponentGeneratorTest(): void + { + vfsStream::setup('DistributionPackages', null, [ + 'Vendor.Site' => [], + 'Vendor.Default' => [], + ]); + + $this->prophet = new Prophet(); + + $this->sitePackage = $this->prophet->prophesize(FlowPackageInterface::class); + $this->sitePackage + ->getPackageKey() + ->willReturn('Vendor.Site'); + $this->sitePackage + ->getPackagePath() + ->willReturn('vfs://DistributionPackages/Vendor.Site/'); + + $this->defaultPackage = $this->prophet->prophesize(FlowPackageInterface::class); + $this->defaultPackage + ->getPackageKey() + ->willReturn('Vendor.Default'); + $this->defaultPackage + ->getPackagePath() + ->willReturn('vfs://DistributionPackages/Vendor.Default/'); + + $this->packageResolver = $this->prophet->prophesize(PackageResolverInterface::class); + $this->packageResolver + ->resolvePackage('Vendor.Site') + ->willReturn($this->sitePackage); + $this->packageResolver + ->resolvePackage(null) + ->willReturn($this->defaultPackage); + + $this->valueGenerator = new ValueGenerator(new \DateTimeImmutable('@1602423895')); + + $this->inject($this->valueGenerator, 'packageResolver', $this->packageResolver->reveal()); + } + + /** + * @after + * @return void + */ + public function tearDownComponentGeneratorTest(): void + { + $this->prophet->checkPredictions(); + } + + /** + * @return array + */ + public function exampleProvider(): array + { + return [ + 'headlinetype' => + ['Headline', 'HeadlineType', 'string', ['H1', 'H2', 'DIV'], 'Vendor.Site', [ + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Headline/HeadlineType.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Presentation/Headline/HeadlineTypeIsInvalid.php', + 'vfs://DistributionPackages/Vendor.Site/Classes/Application/HeadlineTypeProvider.php', + ]], + 'trafficlight' => + ['Crossing', 'TrafficLight', 'int', ['RED', 'YELLOW', 'GREEN'], null, [ + 'vfs://DistributionPackages/Vendor.Default/Classes/Presentation/Crossing/TrafficLight.php', + 'vfs://DistributionPackages/Vendor.Default/Classes/Presentation/Crossing/TrafficLightIsInvalid.php', + 'vfs://DistributionPackages/Vendor.Default/Classes/Application/TrafficLightProvider.php', + ]], + ]; + } + + /** + * @test + * @group isolated + * @dataProvider exampleProvider + * @param string $componentName + * @param string $name + * @param string $type + * @param string[] $values + * @param string|null $packageKey + * @param string[] $expectedFileNames + * @return void + */ + public function generatesValues(string $componentName, string $name, string $type, array $values, ?string $packageKey, array $expectedFileNames): void + { + $this->valueGenerator->generateValue($componentName, $name, $type, $values, $packageKey); + + foreach ($expectedFileNames as $fileName) { + $this->assertFileExists($fileName); + $this->assertMatchesSnapshot(file_get_contents($fileName)); + } + } +} diff --git a/Tests/Unit/Domain/Value/ValueTest.php b/Tests/Unit/Domain/Value/ValueTest.php index 8bd4768..8b3f2c4 100644 --- a/Tests/Unit/Domain/Value/ValueTest.php +++ b/Tests/Unit/Domain/Value/ValueTest.php @@ -133,11 +133,11 @@ final class MyComponentTypeIsInvalid extends \DomainException { public static function becauseItMustBeOneOfTheDefinedConstants(string $attemptedValue): self { - return new self(\'The given value "\' . $attemptedValue . \'" is no valid MyComponentType, must be one of the defined constants. \', ' . time() . '); + return new self(\'The given value "\' . $attemptedValue . \'" is no valid MyComponentType, must be one of the defined constants. \', 1602424261); } } ', - $this->subject->getExceptionContent() + $this->subject->getExceptionContent(new \DateTimeImmutable('@1602424261')) ); } diff --git a/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__1.txt b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__1.txt new file mode 100644 index 0000000..15ae768 --- /dev/null +++ b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__1.txt @@ -0,0 +1,89 @@ +value = $value; + } + + public static function fromString(string $string): self + { + if (!in_array($string, self::getValues())) { + throw HeadlineTypeIsInvalid::becauseItMustBeOneOfTheDefinedConstants($string); + } + + return new self($string); + } + + public static function H1(): self + { + return new self(self::TYPE_H1); + } + + public static function H2(): self + { + return new self(self::TYPE_H2); + } + + public static function DIV(): self + { + return new self(self::TYPE_DIV); + } + + public function getIsH1(): bool + { + return $this->value === self::TYPE_H1; + } + + public function getIsH2(): bool + { + return $this->value === self::TYPE_H2; + } + + public function getIsDIV(): bool + { + return $this->value === self::TYPE_DIV; + } + + /** + * @return array|string[] + */ + public static function getValues(): array + { + return [ + self::TYPE_H1, + self::TYPE_H2, + self::TYPE_DIV + ]; + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__2.txt b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__2.txt new file mode 100644 index 0000000..a8e3642 --- /dev/null +++ b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set headlinetype__2.txt @@ -0,0 +1,19 @@ +translator->translateById('headlineType.' . $value, [], null, null, 'Headline', 'Vendor.Site') ?: $value; + } + + return $headlineTypes; + } + + /** + * @return array|string[] + */ + public function getValues(): array + { + return HeadlineType::getValues(); + } + + public function allowsCallOfMethod($methodName): bool + { + return true; + } +} diff --git a/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__1.txt b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__1.txt new file mode 100644 index 0000000..6f0b8a2 --- /dev/null +++ b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__1.txt @@ -0,0 +1,84 @@ +value = $value; + } + + public static function fromInt(int $int): self + { + if (!in_array($int, self::getValues())) { + throw TrafficLightIsInvalid::becauseItMustBeOneOfTheDefinedConstants($int); + } + + return new self($int); + } + + public static function RED(): self + { + return new self(self::LIGHT_RED); + } + + public static function YELLOW(): self + { + return new self(self::LIGHT_YELLOW); + } + + public static function GREEN(): self + { + return new self(self::LIGHT_GREEN); + } + + public function getIsRED(): bool + { + return $this->value === self::LIGHT_RED; + } + + public function getIsYELLOW(): bool + { + return $this->value === self::LIGHT_YELLOW; + } + + public function getIsGREEN(): bool + { + return $this->value === self::LIGHT_GREEN; + } + + /** + * @return array|int[] + */ + public static function getValues(): array + { + return [ + self::LIGHT_RED, + self::LIGHT_YELLOW, + self::LIGHT_GREEN + ]; + } + + public function getValue(): int + { + return $this->value; + } +} diff --git a/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__2.txt b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__2.txt new file mode 100644 index 0000000..c18d931 --- /dev/null +++ b/Tests/Unit/Domain/Value/__snapshots__/ValueGeneratorTest__generatesValues with data set trafficlight__2.txt @@ -0,0 +1,19 @@ +translator->translateById('trafficLight.' . $value, [], null, null, 'Crossing', 'Vendor.Default') ?: $value; + } + + return $trafficLights; + } + + /** + * @return array|int[] + */ + public function getValues(): array + { + return TrafficLight::getValues(); + } + + public function allowsCallOfMethod($methodName): bool + { + return true; + } +} diff --git a/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php index 5122225..e536c4e 100644 --- a/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php +++ b/Tests/Unit/Fusion/AbstractComponentPresentationObjectFactoryTest.php @@ -104,11 +104,12 @@ public function createWrapperForTest(TraversableNodeInterface $node, Presentatio /** * @param TraversableNodeInterface $node * @param string $propertyName + * @param boolean $block * @return string */ - public function getEditablePropertyForTest(TraversableNodeInterface $node, string $propertyName): string + public function getEditablePropertyForTest(TraversableNodeInterface $node, string $propertyName, bool $block): string { - return $this->getEditableProperty($node, $propertyName); + return $this->getEditableProperty($node, $propertyName, $block); } /** @@ -170,7 +171,7 @@ public function createsWrappersForSelfWrappingTrait(): void * @test * @return void */ - public function providesEditableNodeProperties(): void + public function providesInlineEditableNodeProperties(): void { $textNode = $this->prophet ->prophesize(TraversableNodeInterface::class) @@ -183,7 +184,28 @@ public function providesEditableNodeProperties(): void $this->assertEquals( '

Lorem ipsum...

', - $factory->getEditablePropertyForTest($textNode->reveal(), 'content') + $factory->getEditablePropertyForTest($textNode->reveal(), 'content', false) + ); + } + + /** + * @test + * @return void + */ + public function providesBlockEditableNodeProperties(): void + { + $textNode = $this->prophet + ->prophesize(TraversableNodeInterface::class) + ->willImplement(NodeInterface::class); + $textNode->getIdentifier()->willReturn('text-node'); + $textNode->getProperty('content')->willReturn('

Lorem ipsum...

'); + + /** @var mixed $factory */ + $factory = $this->factory; + + $this->assertEquals( + '

Lorem ipsum...

', + $factory->getEditablePropertyForTest($textNode->reveal(), 'content', true) ); } diff --git a/composer.json b/composer.json index 23b81cc..895c3f4 100755 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "neos/buildessentials": "^6.3", "mikey179/vfsstream": "^1.6", "squizlabs/php_codesniffer": "^3.5", - "jangregor/phpstan-prophecy": "^0.8.0" + "jangregor/phpstan-prophecy": "^0.8.0", + "spatie/phpunit-snapshot-assertions": "^4.2" }, "config": { "vendor-dir": "Packages/Libraries", diff --git a/phpunit.xml b/phpunit.xml index 57db207..16d31d4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -17,7 +17,7 @@ processUncoveredFiles="true" pathCoverage="true" ignoreDeprecatedCodeUnits="true" - disableCodeCoverageIgnore="true"> + disableCodeCoverageIgnore="false"> Classes From 76c2c7f0c327c2de00f0faee486283a12d894d77 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 16:04:11 +0200 Subject: [PATCH 21/28] Fix code style --- Classes/Domain/Component/Component.php | 6 +++--- Classes/Domain/Component/ComponentRepository.php | 2 +- .../Domain/Component/PropTypeRepositoryInterface.php | 1 - Tests/Unit/Domain/Component/ComponentTest.php | 12 ++++++++---- Tests/Unit/Domain/Value/ValueTest.php | 12 ++++++++---- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Classes/Domain/Component/Component.php b/Classes/Domain/Component/Component.php index 3c5b4f3..997fb8b 100755 --- a/Classes/Domain/Component/Component.php +++ b/Classes/Domain/Component/Component.php @@ -186,7 +186,7 @@ public function getInterfaceContent(): string ' . $this->renderUseStatements() . ' interface ' . $this->getName() . 'Interface extends ComponentPresentationObjectInterface { - ' . trim (implode("\n\n ", $this->getAccessors(true))) . ' + ' . trim(implode("\n\n ", $this->getAccessors(true))) . ' } '; } @@ -211,11 +211,11 @@ public function getClassContent(): string */ final class ' . $this->getName() . ' extends AbstractComponentPresentationObject implements ' . $this->getName() . 'Interface { - ' . trim (implode("\n\n ", $this->getProperties())) . ' + ' . trim(implode("\n\n ", $this->getProperties())) . ' ' . $this->renderConstructor() . ' - ' . trim (implode("\n\n ", $this->getAccessors(false))) . ' + ' . trim(implode("\n\n ", $this->getAccessors(false))) . ' } '; } diff --git a/Classes/Domain/Component/ComponentRepository.php b/Classes/Domain/Component/ComponentRepository.php index 03d2ef2..df2b201 100755 --- a/Classes/Domain/Component/ComponentRepository.php +++ b/Classes/Domain/Component/ComponentRepository.php @@ -24,7 +24,7 @@ final class ComponentRepository public function getComponentType(string $interfaceName): ComponentType { $reflection = new \ReflectionClass($interfaceName); - foreach($reflection->getMethods() as $method) { + foreach ($reflection->getMethods() as $method) { if (\mb_strpos($method->getName(), 'get') === 0 && interface_exists((string) $method->getReturnType())) { /** @phpstan-var class-string $getterReturnTypeName */ $getterReturnTypeName = (string) $method->getReturnType(); diff --git a/Classes/Domain/Component/PropTypeRepositoryInterface.php b/Classes/Domain/Component/PropTypeRepositoryInterface.php index 6272f6e..03f01d5 100644 --- a/Classes/Domain/Component/PropTypeRepositoryInterface.php +++ b/Classes/Domain/Component/PropTypeRepositoryInterface.php @@ -2,7 +2,6 @@ namespace PackageFactory\AtomicFusion\PresentationObjects\Domain\Component; - /** * The interface to be implemented by prop type repositories */ diff --git a/Tests/Unit/Domain/Component/ComponentTest.php b/Tests/Unit/Domain/Component/ComponentTest.php index 6bf726d..13b628b 100644 --- a/Tests/Unit/Domain/Component/ComponentTest.php +++ b/Tests/Unit/Domain/Component/ComponentTest.php @@ -53,7 +53,8 @@ public function setUp(): void public function testGetInterfaceContent(): void { - Assert::assertSame('subject->getProviderContent()); + $this->subject->getProviderContent() + ); } } From 33e9e8a912a56ae38e0ae86095eb420a0e6bf39e Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 16:06:36 +0200 Subject: [PATCH 22/28] Increase php version for qa action --- .github/workflows/qa.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 61a5f21..f2487a5 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -12,7 +12,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.3' + php-version: '7.4' extensions: mbstring, intl tools: phpcs @@ -33,7 +33,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.3' + php-version: '7.4' extensions: mbstring, intl - name: Get composer cache directory @@ -71,7 +71,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.3' + php-version: '7.4' extensions: mbstring, intl coverage: xdebug @@ -95,4 +95,4 @@ jobs: bin/phpunit -c phpunit.xml \ --enforce-time-limit \ --coverage-text \ - Tests \ No newline at end of file + Tests From e153b71e2fc01ce89ee2af30526fadbe16a18aaa Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Sun, 11 Oct 2020 16:33:49 +0200 Subject: [PATCH 23/28] Add declare(strict_types=1); to all PHP files --- Classes/Command/ComponentCommandController.php | 3 +-- Classes/Domain/Component/Component.php | 8 ++++---- Classes/Domain/Component/ComponentGenerator.php | 5 ++--- Classes/Domain/Component/ComponentRepository.php | 3 +-- Classes/Domain/Component/PropType.php | 2 +- Classes/Domain/Component/PropTypeClass.php | 2 +- Classes/Domain/Component/PropTypeIdentifier.php | 2 +- Classes/Domain/Component/PropTypeIsInvalid.php | 3 +-- Classes/Domain/Component/PropTypeRepository.php | 3 +-- Classes/Domain/Component/PropTypeRepositoryInterface.php | 3 +-- Classes/Domain/NoPackageCouldBeResolved.php | 3 +-- Classes/Domain/PackageResolver.php | 3 +-- Classes/Domain/Value/Value.php | 2 +- Classes/Domain/Value/ValueGenerator.php | 3 +-- Classes/Fusion/AbstractComponentPresentationObject.php | 3 +-- .../Fusion/AbstractComponentPresentationObjectFactory.php | 3 +-- ...resentationObjectDoesNotImplementRequiredInterface.php | 3 +-- .../ComponentPresentationObjectFactoryInterface.php | 3 +-- Classes/Fusion/ComponentPresentationObjectInterface.php | 3 +-- .../ComponentPresentationObjectInterfaceIsMissing.php | 3 +-- .../ComponentPresentationObjectInterfaceIsUndeclared.php | 3 +-- Classes/Fusion/ComponentPresentationObjectIsMissing.php | 3 +-- .../Fusion/PresentationObjectComponentImplementation.php | 3 +-- Classes/Fusion/SelfWrapping.php | 3 +-- Classes/Fusion/UriServiceInterface.php | 2 +- Classes/Infrastructure/UriService.php | 2 +- Tests/Unit/Domain/Component/ComponentTest.php | 2 +- Tests/Unit/Domain/Value/ValueTest.php | 2 +- .../PresentationObjectComponentImplementationTest.php | 2 +- Tests/Unit/Helper/DummyContentElementEditableService.php | 2 +- Tests/Unit/Helper/DummyContext.php | 2 +- Tests/Unit/Helper/DummyNode.php | 2 +- Tests/Unit/Helper/DummyPropTypeRepository.php | 2 +- 33 files changed, 37 insertions(+), 56 deletions(-) diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index 07d83a7..a95007a 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -1,5 +1,4 @@ -getType()) . '/' . $this->name . '/' . $this->name . '.fusion'; + return $packagePath . 'Resources/Private/Fusion/Presentation/' . ucfirst((string) $this->getType()) . '/' . $this->name . '/' . $this->name . '.fusion'; } /** @@ -251,7 +251,7 @@ public function getFusionContent(): string if ($propType->getFullyQualifiedName() === ImageSourceHelperInterface::class) { $definitionData = 'isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; } elseif ($propType->getClass()->isComponent()) { - $definitionData = '<' . $this->packageKey . ':' . ucfirst($propType->getClass()) . '.' . $propType->getName() . ' presentationObject={presentationObject.' . $propName . '}' . ($propType->isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; + $definitionData = '<' . $this->packageKey . ':' . ucfirst((string) $propType->getClass()) . '.' . $propType->getName() . ' presentationObject={presentationObject.' . $propName . '}' . ($propType->isNullable() ? ' @if.isToBeRendered={presentationObject.' . $propName. '}' : '') . ' />'; } else { $definitionData = '{presentationObject.' . $propName . '}'; } @@ -260,7 +260,7 @@ public function getFusionContent(): string
' . $definitionData . '
'; } - return 'prototype(' . $this->packageKey . ':' . ucfirst($this->getType()) . '.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + return 'prototype(' . $this->packageKey . ':' . ucfirst((string) $this->getType()) . '.' . $this->name . ') < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { @presentationObjectInterface = \'' . str_replace('\\', '\\\\', ucfirst($this->getNamespace())) . '\\\\' . $this->name . 'Interface\' @styleguide { diff --git a/Classes/Domain/Component/ComponentGenerator.php b/Classes/Domain/Component/ComponentGenerator.php index 21fb777..2ba3fcc 100755 --- a/Classes/Domain/Component/ComponentGenerator.php +++ b/Classes/Domain/Component/ComponentGenerator.php @@ -1,5 +1,4 @@ -getType()) . '/' . $componentName; + $fusionPath = $packagePath . 'Resources/Private/Fusion/Presentation/' . ucfirst((string) $component->getType()) . '/' . $componentName; if (!file_exists($fusionPath)) { Files::createDirectoryRecursively($fusionPath); } diff --git a/Classes/Domain/Component/ComponentRepository.php b/Classes/Domain/Component/ComponentRepository.php index df2b201..375a7eb 100755 --- a/Classes/Domain/Component/ComponentRepository.php +++ b/Classes/Domain/Component/ComponentRepository.php @@ -1,5 +1,4 @@ - Date: Sun, 11 Oct 2020 16:38:56 +0200 Subject: [PATCH 24/28] Add package signature to all PHP files --- .gitignore | 2 ++ Classes/Domain/Component/PropTypeRepositoryInterface.php | 4 ++++ Tests/Unit/Domain/Component/ComponentTest.php | 4 ++++ Tests/Unit/Domain/Value/ValueTest.php | 4 ++++ Tests/Unit/Helper/DummyContentElementEditableService.php | 4 ++++ Tests/Unit/Helper/DummyContext.php | 4 ++++ Tests/Unit/Helper/DummyNode.php | 4 ++++ Tests/Unit/Helper/DummyPropTypeRepository.php | 4 ++++ 8 files changed, 30 insertions(+) diff --git a/.gitignore b/.gitignore index c4c9708..38a478a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ bin/ Build/ Packages/ +Web/ +Data/ vendor/ composer.lock .phpunit.result.cache diff --git a/Classes/Domain/Component/PropTypeRepositoryInterface.php b/Classes/Domain/Component/PropTypeRepositoryInterface.php index 4758317..a654db7 100644 --- a/Classes/Domain/Component/PropTypeRepositoryInterface.php +++ b/Classes/Domain/Component/PropTypeRepositoryInterface.php @@ -1,6 +1,10 @@ Date: Sun, 11 Oct 2020 17:16:25 +0200 Subject: [PATCH 25/28] Decouple PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent from Neos.Fusion:Component --- ...sentationObjectComponentImplementation.php | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/Classes/Fusion/PresentationObjectComponentImplementation.php b/Classes/Fusion/PresentationObjectComponentImplementation.php index 6928ecb..4a3ae5b 100755 --- a/Classes/Fusion/PresentationObjectComponentImplementation.php +++ b/Classes/Fusion/PresentationObjectComponentImplementation.php @@ -5,10 +5,12 @@ * This file is part of the PackageFactory.AtomicFusion.PresentationObjects package */ +use Neos\Fusion\FusionObjects\DataStructureImplementation; + /** * A custom component implementation allowing the usage of presentation objects in the fusion runtime */ -class PresentationObjectComponentImplementation extends \Neos\Fusion\FusionObjects\ComponentImplementation +class PresentationObjectComponentImplementation extends DataStructureImplementation { const PREVIEW_MODE = 'isInPreviewMode'; @@ -16,6 +18,27 @@ class PresentationObjectComponentImplementation extends \Neos\Fusion\FusionObjec const INTERFACE_DECLARATION_NAME = '__meta/presentationObjectInterface'; + /** + * Properties that are ignored and not included into the ``props`` context + * + * @var array|string[] + */ + protected $ignoreProperties = ['__meta', 'renderer']; + + /** + * Evaluate the fusion-keys and transfer the result into the context as ``props`` + * afterwards evaluate the ``renderer`` with this context + * + * @return mixed + */ + public function evaluate() + { + $context = $this->runtime->getCurrentContext(); + $renderContext = $this->prepare($context); + $result = $this->render($renderContext); + return $result; + } + /** * Prepare the context for the renderer * @@ -24,7 +47,7 @@ class PresentationObjectComponentImplementation extends \Neos\Fusion\FusionObjec * @phpstan-return array * @return array */ - protected function prepare($context) + protected function prepare(array $context): array { if ($this->isInPreviewMode()) { $props = $this->getProps(); @@ -37,7 +60,45 @@ protected function prepare($context) $context[self::OBJECT_NAME] = $this->getPresentationObject(); } - return parent::prepare($context); + $context['props'] = $this->getProps(); + return $context; + } + + /** + * Calculate the component props + * + * @phpstan-return array + * @return array + */ + protected function getProps() + { + /** @phpstan-var string[] $sortedChildFusionKeys */ + $sortedChildFusionKeys = $this->sortNestedFusionKeys(); + $props = []; + foreach ($sortedChildFusionKeys as $key) { + try { + $props[$key] = $this->fusionValue($key); + } catch (\Exception $e) { + $props[$key] = $this->runtime->handleRenderingException($this->path . '/' . $key, $e); + } + } + + return $props; + } + + /** + * Evaluate the renderer with the give context and return + * + * @phpstan-param array $context + * @param array $context + * @return mixed + */ + protected function render(array $context) + { + $this->runtime->pushContextArray($context); + $result = $this->runtime->render($this->path . '/renderer'); + $this->runtime->popContext(); + return $result; } /** From b9e272c5113caeb303fe205fff593b05a8e97d78 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Mon, 12 Oct 2020 00:00:08 +0200 Subject: [PATCH 26/28] Finalize Documentation --- .../Command/ComponentCommandController.php | 26 ++ Documentation/00_Index.md | 5 + .../01_PresentationObjectsAndComponents.md | 180 ++++++++ .../02_PresentationObjectFactories.md | 93 ++++ Documentation/03_IntegrationRecipes.md | 260 +++++++++++ Documentation/04_Kickstarter.md | 407 ++++++++++++++++++ Documentation/05_PreviewMode.md | 36 ++ README.md | 267 +++--------- 8 files changed, 1072 insertions(+), 202 deletions(-) create mode 100644 Documentation/00_Index.md create mode 100644 Documentation/01_PresentationObjectsAndComponents.md create mode 100644 Documentation/02_PresentationObjectFactories.md create mode 100644 Documentation/03_IntegrationRecipes.md create mode 100644 Documentation/04_Kickstarter.md create mode 100644 Documentation/05_PreviewMode.md diff --git a/Classes/Command/ComponentCommandController.php b/Classes/Command/ComponentCommandController.php index a95007a..b32baa1 100755 --- a/Classes/Command/ComponentCommandController.php +++ b/Classes/Command/ComponentCommandController.php @@ -30,6 +30,24 @@ class ComponentCommandController extends CommandController /** * Create a new PresentationObject component and factory * + * This command will create an interface, a value object and a + * factory under in the chosen component namespace. It'll also register + * the factory for later use in Fusion. + * + * The remaining arguments of this command are interpreted as a list of + * property descriptors which consist of a property name and a type name + * separated by a colon (e.g.: "title:string"). + * + * The following values are allowed for types: + * + * * string, int, float, bool + * * Value class names created with component:kickstartvalue in the same + * component namespace + * * Component class names created with component:kickstart in the same + * package + * * ImageSource + * * Uri + * * @param string $name The name of the new component * @param null|string $packageKey Package key of an optional target package, if not set the configured default package or the first available site package will be used * @return void @@ -42,6 +60,14 @@ public function kickStartCommand(string $name, ?string $packageKey = null): void /** * Create a new pseudo-enum value object * + * This command will create a value object for a pseudo-enum under in the + * chosen component namespace and under the provided name. It'll also create a + * co-located exception class that will be used when validation for the + * pseudo-enum fails. + * + * Additionally, a datasource for use in SelectBoxEditors will be created + * in the Application namespace of your chosen package. + * * @param string $componentName The name of the component the new pseudo-enum belongs to * @param string $name The name of the new pseudo-enum * @param string $type The type of the new pseudo-enum (must be one of: "string", "int") diff --git a/Documentation/00_Index.md b/Documentation/00_Index.md new file mode 100644 index 0000000..45305b6 --- /dev/null +++ b/Documentation/00_Index.md @@ -0,0 +1,5 @@ +1. [PresentationObjects and Components](./01_PresentationObjectsAndComponents.md) +2. [Content integration with PresentationObject Factories](./02_PresentationObjectFactories.md) +3. [Integration Recipes](./03_IntegrationRecipes.md) +4. [Scaffolding with the Component Kickstarter](./04_Kickstarter.md) +5. [Preview Mode](./05_PreviewMode.md) \ No newline at end of file diff --git a/Documentation/01_PresentationObjectsAndComponents.md b/Documentation/01_PresentationObjectsAndComponents.md new file mode 100644 index 0000000..28b882e --- /dev/null +++ b/Documentation/01_PresentationObjectsAndComponents.md @@ -0,0 +1,180 @@ + + +--- + +# 1. PresentationObjects and Components + +> **Hint:** This section describes the manual creation of PresentationObjects and PresentationObject components. Both patterns can also be scaffolded by the [Kickstarter](./04_Kickstarter.md). + +In this tutorial, we're going to write a PresentationObject for an image component. Our image consists of a `src`, an `alt` and an optional `title` property. + +## Writing the PresentationObject + +The most important function of PresentationObjects is to enforce the interface between domain and presentation layer. + +For a single component that interface is represented by an actual PHP interface. So let's start with that: + +*`EXAMPLE: PresentationObject Interface`* + +```php +*`EXAMPLE: PresentationObject`* + +```php +src = $src; + $this->alt = $alt; + $this->title = $title; + } + + /** + * @return string + */ + public function getSrc(): string + { + return $this->src; + } + + /** + * @return string + */ + public function getAlt(): string + { + return $this->alt; + } + + /** + * Tip: + * PresentationObjects are immutable. In order to perform change actions + * you need to implement a copy-on-write mechanism like this one. + * + * Such with*-methods are optional however. + * + * @param string $alt + * @return self + */ + public function withAlt(string $alt): self + { + return new self($this->src, $alt, $this->title); + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } +} +``` + +## Writing the PresentationObject component + +For our component we need to extend `PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent`, which works similarly to `Neos.Fusion:Component`. + +The first difference to `Neos.Fusion:Component` is the mandatory `@presentationObjectInterface` annotation which connects our component to the PHP interface from above. + +The second difference is, that besides the usual `props`-Context, your renderer can now also access the special `presentationObject`-Context, which holds our verified data. + +*`EXAMPLE: Resources/Private/Fusion/Presentation/Leaf/Image/Image.fusion`* + +```fusion +prototype(Vendor.Site:Leaf.Image) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = 'Vendor\\Site\\Presentation\\Image\\ImageInterface' + + renderer = afx` + {presentationObject.alt} + ` +} +``` + +That's it! Our `Vendor.Site:Leaf.Image` can now be used like this (AFX): + +```afx +myImage = afx`` +``` + +Or like this (Plain Fusion): + +```fusion +myImage = Vendor.Site:Leaf.Image { + presentationObject = ${someObject} +} +``` + +An exception will be thrown, if `someObject` does not implement `Vendor\Site\Presentation\Image\ImageInterface`. + +--- + + \ No newline at end of file diff --git a/Documentation/02_PresentationObjectFactories.md b/Documentation/02_PresentationObjectFactories.md new file mode 100644 index 0000000..be073ba --- /dev/null +++ b/Documentation/02_PresentationObjectFactories.md @@ -0,0 +1,93 @@ + + +--- + +# 2. Content integration with PresentationObject Factories + +> **Hint:** If you used the [Kickstarter](./Kickstarter.md) to create your component, an empty factory has already been created alongside the PresentationObject. + +In this tutorial, we're going to write a PresentationObject factory for the image component we've created in "[PresentationObjects and Components](./PresentationObjectsAndComponents.md)". Let's assume that we have a `Vendor.Site:Content.Image` node type with the properties `image__src`, `image__alt` and `image__title` that we want to integrate with our Image component. + +## Writing the factory + +PresentationObject factories are co-located with their respective PresentationObjects. It's recommended to create factory methods with a speaking name prefixed with `for*` or `from*` to describe their use-case. When your code base grows, the factory will act like an index showing you all the different places in which the respective component is used. + +*`EXAMPLE: PresentationObject Factory`* + +```php +getNodeType()->isOfType('Vendor.Site:Content.Image')); + + return new Image( + $node->getProperty('image__src') + ? $this->uriService->getAssetUri($node->getProperty('image__src')) + : $this->uriService->getDummyImageUri() + $node->getProperty('image__alt') ?? '', + $node->getProperty('image__title') + ); + } +} +``` + +## Registering the factory + +Each factory that extends `AbstractComponentPresentationObjectFactory` automatically implements the `Neos\Eel\ProtectedContextAwareInterface` and can be used as an Eel helper. To make our factory available in Fusion, we need to register it in the Settings: + +*`EXAMPLE: Settings.PresentationHelpers.yaml`* + +```yaml +Neos: + Fusion: + defaultContext: + Vendor.Site.Image: Vendor\Site\Presentation\Image\ImageFactory +``` + +## Connecting the factory to content element rendering + +Neos uses the `Neos.Neos:ContentCase` to map nodes to rendering prototypes. For our `Vendor.Site:Content.Image` node, the entry point is going to be a Fusion prototype of the same name. + +From here, we just need to extend `Neos.Neos:ContentComponent` and provide our PresentationObject component `Vendor.Site:Leaf.Image` as the renderer. As the `presentationObject` we pass the result of the `forImageNode`-method of our newly registered `ImageFactory`. + +*`EXAMPLE: Resources/Private/Fusion/Integration/Content/Image.fusion`* + +```fusion +prototype(Vendor.Site:Content.Image) < prototype(Neos.Neos:ContentComponent) { + renderer = Vendor.Site:Leaf.Image { + presentationObject = ${Vendor.Site.Image.forImageNode(node)} + } +} +``` + +That's it! Our image component is now fully integrated and can be edited in the Neos Backend. + +--- + + \ No newline at end of file diff --git a/Documentation/03_IntegrationRecipes.md b/Documentation/03_IntegrationRecipes.md new file mode 100644 index 0000000..d5cc581 --- /dev/null +++ b/Documentation/03_IntegrationRecipes.md @@ -0,0 +1,260 @@ + + +--- + +# 3. Integration Recipes + +This article discusses various helper APIs you can use for integrating content via PresentationObject factories. + +## Editable properties + +In plain AtomicFusion we would use `Neos.Neos:Editable` to integrate a property that is supposed to be editable via CK Editor in the Neos UI. The analogous mechanism in PresentationObject factories is the built-in protected method `getEditableProperty`. + +*`EXAMPLE: PresentationObject Factory`* + +```php +/* ... */ +final class TextFactory extends AbstractComponentPresentationObjectFactory +{ + /** + * @param TraversableNodeInterface $node + * @return TextInterface + */ + public function forTextNode(TraversableNodeInterface $node): TextInterface + { + // Optional: Use assertions to ensure the incoming node type + assert($node->getNodeType()->isOfType('Vendor.Site:Content.Text')); + + return new Text( + $this->getEditableProperty($node, 'content', true) + ); + } +} +``` + +### `getEditableProperty` Parameters + +| name | type | description | default value | +|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|---------------| +| $node | [`TraversableNodeInterface`](https://github.com/neos/neos-development-collection/blob/master/Neos.ContentRepository/Classes/Domain/Projection/Content/TraversableNodeInterface.php) | The node that holds the property | | +| $propertyName | `string` | The name of the property | | +| $block | `boolean` | If true, an additional `
` is wrapped around the property value (sometimes needed for a proper editing experience) | `false` | + + + +## Find nodes with a filter string + +PresentationObject factories provide the protected method `findChildNodesByNodeTypeFilterString` as a shortcut for filtering the children of a given node. + +### `findChildNodesByNodeTypeFilterString` Parameters + +| name | type | description | default value | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|---------------| +| $parentNode | [`TraversableNodeInterface`](https://github.com/neos/neos-development-collection/blob/master/Neos.ContentRepository/Classes/Domain/Projection/Content/TraversableNodeInterface.php) | The node whose children are to be filtered | | +| $nodeTypeFilterString | `string` | A node type filter string (e.g. `Neos.Neos:Document,!Neos.Neos:Shortcut`) | | + +## Composition + +If you have a component that receives arbitrary content, it's fine to create a fallback, so the content can be passed via `props`: + +*`EXAMPLE: Resources/Private/Fusion/Presentation/Leaf/Container/Container.fusion`* + +```fusion +prototype(Vendor.Site:Leaf.Container) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = 'Vendor\\Site\\Presentation\\Container\\ContainerInterface' + + renderer = afx` +
+ {props.content || presentationObject.content} +
+ ` +} +``` + +In Integration, components can be assembled like this: + +*`EXAMPLE: Resources/Private/Fusion/Integration/Content/Image.fusion`* + +```fusion +prototype(Vendor.Site:Content.Stage) < prototype(Neos.Neos:ContentComponent) { + renderer = Vendor.Site:Leaf.Container { + presentationObject = ${Vendor.Site.Container.forStageNode(node)} + content = Vendor.Site:Composite.TextWithHeadline { + presentationObject = ${Vendor.Site.TextWithHeadline.forStageNode(node)} + } + } +} +``` + +## Applying ContentElementWrapping to nested components + +PresentationObject components are "dumb", in that they should not have access to any data outside their own interface. This leads to a problem when rendering nested content elements that need to be editable on lower levels. + +The Neos UI requires metadata to treat certain DOM nodes as content elements and render things like the blue selection border, the inline toolbar and CK editor. When you render a tree of value objects, the meta information about nodes gets lost on the way, so only the outer-most element is going to be rendered with `ContentElementWrapping`. + +> **Side note:** A similar problem exists in plain AtomicFusion when you try to propagate nested `Neos.Fusion:DataStructure`s through a nested component tree. The solution applied there usually is to add an additional property `__node` to your data structure and apply `ContentElementWrapping` by overriding the target component prototype in place. + +`PackageFactory.AtomicFusion.PresentationObjects` comes with a workaround to address this problem. This section is going to introduce this solution by an example. + +Let's assume, we have two components `Deck` and `Card`. The accompanying `DeckInterface` has a method `getCards` which returns an array of `CardInterface`s. We now want to write a `DeckFactory` method `forTeaserList`, that builds a `Deck` in which every `Card` is editable. + +### The `SelfWrapping` trait + +First, we need to apply the `SelfWrapping` trait to the card component like this: + +*`EXAMPLE: PresentationObject`* + +```php +/* ... */ + +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\SelfWrapping; + +final class Card implements CardInterface +{ + use SelfWrapping; + + public function __construct( + /* ... */ + ?callable $wrapper = null + ) { + /* ... */ + $this->wrapper = $wrapper; + } +} +``` + +The `SelfWrapping` trait introduces a method `wrap` to our `Card` object, that we will later use for `ContentElementWrapping` in Fusion. This is also why we added the `?callable $wrapper = null` parameter to our constructor and assigned it to `$this->wrapper`. The `SelfWrapping` trait will this closure if it is set. + +### `ContentElementWrapping` in Fusion + +Now that we have our `wrap` method, we can use it in Fusion with an `@process` annotation like this: + +*`EXAMPLE: Resources/Private/Fusion/Presentation/Composite/Card/Card.fusion`* + +```fusion +prototype(Vendor.Site:Composite.Card) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + @presentationObjectInterface = 'Vendor\\Site\\Presentation\\Card\\CardInterface' + + renderer.@process.wrap.@position = 'end 9999' + renderer.@process.wrap = ${presentationObject.wrap(value)} +} + +``` + +It's recommended to put this at the very end of your processing-chain via `@position = 'end 9999'` to ensure that it wraps the entire component, including other `@process` wrappings. + +### Creating a wrapper closure in the PresentationObject Factory + +PresentationObject factories provide the protected method `createWrapper` that returns a closure that can be processed by the `SelfWrapping` trait. It takes a `TraversableNodeInterface` and a `PresentationObjectComponentImplementation` as parameters. The latter might seem a little strange, but it's just the PHP instance that represents our PresentationObject component and can be accessed via `this` (see the second example below the next one). + +*`EXAMPLE: PresentationObject Factory`* + +```php +use PackageFactory\AtomicFusion\PresentationObjects\Fusion\PresentationObjectComponentImplementation; + +/* ... */ +final class DeckFactory extends AbstractComponentPresentationObjectFactory +{ + public function forTeaserList(TraversableNodeInterface $teaserListNode, PresentationObjectComponentImplementation $fusionObject): DeckInterface + { + return new Deck( + array_map( + function(TraversableNodeInterface $teaserNode) { + return new Card( + $this->getEditableProperty($teaserNode, 'title'), + $this->createWrapper($teaserNode, $fusionObject) + ); + }, + $teaserListNode->findChildNodes()->toArray() + ) + ); + } +} +``` + +We can now invoke the `forTeaserList` in Fusion. In the example below, you can see how `PresentationObjectComponentImplementation` is obtained from `this`: + +*`EXAMPLE: Resources/Private/Fusion/Integration/Generated/LatestNews.fusion`* + +```fusion +prototype(Vendor.Site:Generated.TeaserList) < prototype(Neos.Neos:ContentComponent) { + renderer = Vendor.Site:Composite.Deck { + presentationObject = ${Vendor.Site.Deck.forTeaserList(node, this)} + } +} +``` + +This solution is a bit elaborate and is likely to be replaced by a more concise method in the future. For the time being, it solves the problem sufficiently. + +## The UriService + +PresentationObject factories come with a built-in UriService that can be accessed via `$this->uriService`. This service covers all your need for creating URIs to nodes, assets and the like. + +The following methods are available: + +### `getNodeUri` + +Generates the URI for a given document node. + +#### Parameters + +| name | type | description | default value | +|------|------|-------------|---------------| +| $documentNode | [`TraversableNodeInterface`](https://github.com/neos/neos-development-collection/blob/master/Neos.ContentRepository/Classes/Domain/Projection/Content/TraversableNodeInterface.php) | The node for which a URI is to be generated | | +| $absolute | `boolean` | If true, an absolute URI will be generated | `false` | + +### `getResourceUri` + +Generates an URI for a static resource. + +#### Parameters + +| name | type | description | default value | +|------|------|-------------|---------------| +| $packageKey | string | The package key for the package containing the static resource | | +| $resourcePath | string | The path to the static resource within the package relative to `Resources/Public` | | + +### `getAssetUri` + +Generates the URI for a given asset. + +#### Parameters + +| name | type | description | default value | +|------|------|-------------|---------------| +| $asset | [`AssetInterface`](https://github.com/neos/neos-development-collection/blob/master/Neos.Media/Classes/Domain/Model/AssetInterface.php) | The asset for which a URI is to be generated | | + +### `getDummyImageBaseUri` + +Provides a URI to a dummy image generated by [Sitegeist.Kaleidoscope](https://github.com/sitegeist/Sitegeist.Kaleidoscope). + +### `getControllerContext` + +Gives you access to a [ControllerContext](https://github.com/neos/flow-development/blob/master/Neos.Flow/Classes/Mvc/Controller/ControllerContext.php), which allows you to generate arbitrary internal URIs with the [UriBuilder](https://github.com/neos/flow-development-collection/blob/master/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php). + +### `resolveLinkUri` + +Resolves URIs with the special `asset://` and `node://` protocols. + +#### Parameters + +| name | type | description | default value | +|------|------|-------------|---------------| +| $rawLinkUri | string | The string containing the URI | | +| $subgraph | [ContentContext](https://github.com/neos/neos-development-collection/blob/master/Neos.Neos/Classes/Domain/Service/ContentContext.php) | A reference content context required to resolve `node://` URIs | | + +--- + + \ No newline at end of file diff --git a/Documentation/04_Kickstarter.md b/Documentation/04_Kickstarter.md new file mode 100644 index 0000000..ebbf5d3 --- /dev/null +++ b/Documentation/04_Kickstarter.md @@ -0,0 +1,407 @@ +
+ < 3. Integration Recipes +    |    + Index +    |    + 5. Preview Mode > +
+ +--- + +# Scaffolding with the Component Kickstarter + +Due to the elaborate nature of PresentationObjects `PackageFactory.AtomicFusion.PresentationObjects` ships with a scaffolding tool that eases the creation of all required code patterns. This tool comes in the form of a set of Neos.Flow commands and enables you to generate code from the command line. + +## `component:kickstartvalue` command + +This command generates a new pseudo-enum value object. A pseudo-enum is an attempt to enable enumeration types in PHP, since it doesn't have a native language construct for this (although this might change in the future: https://wiki.php.net/rfc/enum). + +Enumerations (or: enums) can be used to represent discrete values. Think of the state of a traffic light which can only take one of the values red, yellow and green (simplified, of course). A good example in HTML would be the `type` attribute of a `
\ No newline at end of file diff --git a/Documentation/05_PreviewMode.md b/Documentation/05_PreviewMode.md new file mode 100644 index 0000000..b7a739e --- /dev/null +++ b/Documentation/05_PreviewMode.md @@ -0,0 +1,36 @@ +
+ < 4. Kickstarter +    |    + Index +
+ +--- + +# Preview Mode + +The `PresentationObjectComponent` has a special flag to change its behavior when used with tools like Sitegeist.Monocle. + +Sitegeist.Monocle uses dummy data that is read directly from an annotation within the component code. That data ends up being a plain PHP array, that does not implement the desired interface. The PresentationObject enforcement would thus break Sitegeist.Monocle's component preview. + +When the flag `isInPreviewMode` ist set to `true`, the default `props` context +is folded into the `presentationObject` context and the PresentationObject enforcement is deactivated. + +This allows seamless use with tools like Sitegeist.Monocle. + +*`EXAMPLE: Root.fusion`* + +```fusion +prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { + isInPreviewMode = ${request.controllerPackageKey == 'Sitegeist.Monocle'} +} +``` + +> The above example shows how `isInPreviewMode` can be set to true for all PresentationObjectComponents that are rendered in Sitegeist.Monocle. If you only want compatibility with Monocle, just copy the code above and paste it to your `Root.fusion` + +--- + +
+ < 4. Kickstarter +    |    + Index +
\ No newline at end of file diff --git a/README.md b/README.md index 2ff69a4..dd385dc 100644 --- a/README.md +++ b/README.md @@ -10,243 +10,106 @@ composer require packagefactory/atomicfusion-presentationobjects ``` +## Documentation + +1. [PresentationObjects and Components](./Documentation/01_PresentationObjectsAndComponents.md) +2. [Content integration with PresentationObject Factories](./Documentation/02_PresentationObjectFactories.md) +3. [Integration Recipes](./Documentation/03_IntegrationRecipes.md) +4. [Scaffolding with the Component Kickstarter](./Documentation/04_Kickstarter.md) +5. [Preview Mode](./Documentation/05_PreviewMode.md) + ## Why -PackageFactory.AtomicFusion has been the first step in the direction of Component architecture in Neos CMS. It provided a `Component` fusion prototype that allowed for writing frontend components with a clear interface for the backend. - -However, that interface hasn't been strict. Developers were able to express requirements for their components, but those requirements weren't enforced on any level. - -Because of that, the concept of PropTypes were adopted from React.js in the form of PackageFactory.AtomicFusion.PropTypes. PropTypes check incoming data against a defined schema whenever a component is invoked at runtime, thus ensuring that a component can never be rendered with invalid data. - -With the advent of Typescript PropTypes have become sort-of obsolete in the React world, since static typings do not have an impact on bundle size and catch type-related bugs before runtime. - -TODO: DDD tactical pattern ValueObject - -### Benefits - -TODO - -### Drawbacks - -TODO - -## Usage - -### Writing a PresentationObject - -PresentationObject are ValueObjects. In that they are immutable and can only consist of scalar properties or other value objects. - -*`EXAMPLE: PresentationObject`* - -```php -firstProperty = $firstProperty; - $this->secondProperty = $secondProperty; - } - - /** - * @return string - */ - public function getFirstProperty(): string - { - return $this->firstProperty; - } - - /** - * PresentationObjects are immutable. In order to perform change actions - * you need to implement a copy-on-write mechanism like this one. - * - * Such with*-methods are optional however. - * - * @param string $firstProperty - * @return self - */ - public function withFirstProperty(string $firstProperty): self - { - return new self($firstProperty, $this->secondProperty); - } - - /** - * @return integer - */ - public function getSecondProperty(): int - { - return $this->secondProperty; - } -} -``` +`PackageFactory.AtomicFusion` has been the first step in the direction of Component architecture in Neos CMS. It provides a `Component` fusion prototype that allows for writing frontend components with a clear interface for the backend. -### Binding a PresentationObject to a PresentationObjectComponent +However, that interface isn't strict. Developers are able to express requirements for their components, but those requirements aren't enforced on any level. Because of that, [PackageFactory.AtomicFusion.PropTypes](https://github.com/PackageFactory/atomic-fusion-proptypes) was created to bring the concept of [React.js PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html) to AtomicFusion. -*`EXAMPLE: PresentationObject Interface`* +PropTypes check incoming data against a defined schema whenever a component is invoked, thus ensuring that a component can never be rendered with invalid data. The weakness of this pattern is that any guarantee over the integrity of the component interface can only ever be made at runtime. This way integration remains error-prone. -```php -*`EXAMPLE: PresentationObject Component`* - -```fusion -prototype(Vendor.Site:MyPresentationObject) < prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { - @presentationObjectInterface = 'Vendor\\Site\\Presentation\\MyPresentationObject\\MyPresentationObjectInterface' - - renderer = afx` -
-
First property:
-
{presentationObject.firstProperty}
-
Second property:
-
{presentationObject.secondProperty}
-
- ` -} -``` +## How does it work? -TODO +This package provides a special component prototype for Fusion that allows to associate a component with a PHP interface via the `@presentationObjectInterface` annotation. `PackageFactory.AtomicFusion.PresentationObjects` then makes sure that any object that is passed to that component implements the declared interface. -### Writing and registering a PresentationObject Factory +PresentationObjects are Value Objects (see: https://martinfowler.com/bliki/ValueObject.html). They are immutable and are only allowed to consist of scalar properties, other value objects or arrays of the former two. They act as predictable data containers. -*`EXAMPLE: PresentationObject Factory`* +PresentationObjects are created by factories (see: https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)). These classes have the responsibility to encapsulate specific use cases of a component, retrieve all the data needed for producing it and do the required data mapping. In order to access them in Fusion, PresentationObject factories are registered as Eel Helpers. -```php -*`EXAMPLE: Settings.PresentationHelpers.yaml`* +Luckily, there's tools like [phpstan](https://phpstan.org/) or [psalm](https://psalm.dev/), which allow static analysis of your PHP code base. -```yaml -Neos: - Fusion: - defaultContext: - Vendor.Site.MyPresentationObject: Vendor\Site\Presentation\MyPresentationObject\MyPresentationObjectFactory -``` +Typesafety and static analysis comes with a lot of benefits: -TODO +1. **Catch type-related bugs before runtime.** Consequent use of [Typehints](https://docs.phpdoc.org/latest/guides/types.html) ensures the correctness of your code during static analysis. Using [phpDoc types](https://docs.phpdoc.org/latest/guides/types.html) allows you to even go beyond the capabilities of PHP and use patterns like Generics or Union types without them being actually supported by PHPs type system. +2. **Self-documenting interfaces.** Typehints and type annotations amend parameters and properties with the important information of what kind of data they require without the need to look it up in a separate documentation. +3. **IDE support.** Modern PHP IDEs understand Typehints and phpDoc types and can use them to provide code completion, intelligent parameter suggestions and advanced refactoring capabilities. -### Neos CMS content integration with PresentationObject Factories +### Testing & QA Tooling -TODO +Since they're just PHP code, it is quite easy to write functional and unit tests for PresentationObjects and PresentationObject factories with tools like phpunit (https://phpunit.de/). -*`EXAMPLE: MyContentElement.fusion`* +For the same reason, PresentationObjects integrate well with any QA tooling for PHP, like: -```fusion -prototype(Vendor.Site:MyContentElement) < prototype(Neos.Neos:ContentComponent) { - renderer = Vendor.Site:MyPresentationObject { - presentationObject = ${Vendor.Site.MyPresentationObject.forNode(node)} - } -} -``` +* PHP_CodeSniffer: https://github.com/squizlabs/PHP_CodeSniffer +* PHP Coding Standards Fixer: https://cs.symfony.com/ +* PHPCPD: https://github.com/sebastianbergmann/phpcpd -*`EXAMPLE: PresentationObject Factory`* - -```php -/* ... */ -final class MyPresentationObjectFactory extends AbstractComponentPresentationObjectFactory -{ - /** - * @param TraversableNodeInterface $node - * @return MyPresentationObjectInterface - */ - public function forNode(TraversableNodeInterface $node): MyPresentationObjectInterface - { - return new MyPresentationObject( - $node->getProperty('firstProperty'), - $node->getProperty('secondProperty') - ); - } -} -``` +### Separation of Concerns -TODO: see Docs/Integration.md +The extensibility of Fusion is generally a great feature, but it also leads to ambiguity when it comes to complex data processing tasks. -### Using the code generator +You are left with several options to encapsulate effectful tasks (e.g. custom Eel-Helpers, custom FusionObjects, custom FlowQueryOperations, `Neos.Neos:Plugin`, etc.) with no real guidance as to which mechanism fits which use-case. -TODO +When using `PackageFactory.AtomicFusion.PresentationObjects` data and content integration are unambiguously handled by PresentationObject factories. Since these are PHP-Classes capable of any data operation within a Neos instance, your choice of mechanism boils down to one option. -```sh -./flow component:kickstartvalue --package-key=Vendor.Site \ - Headline \ - HeadlineLook string \ - --values=REGULAR,HERO -``` +### Debugging -```sh -./flow component:kickstart --package-key=Vendor.Site - Headline \ - content:string \ - look:HeadlineLook -``` +Fusion is sometimes hard to debug, especially if it is unclear, where exactly a malfunction occurs. `Neos.Fusion:Debug` cannot be arbitrarily positioned in your Fusion code and needs to be rendered just like everything else. it therefore also requires the rendering process to succeed at all. -### Preview Mode +Presentation object factories allow use of the good ol' `\Neos\Flow\var_dump(); die;`-Pattern for simple debugging. -The `PresentationObjectComponent` has a special flag to change its behavior when used with tools like Sitegeist.Monocle. +For more advanced needs the PHP-native character of PresentationObjects comes with a natural compatibility with Xdebug step debugging (https://xdebug.org/docs/remote) and profiling (https://xdebug.org/docs/profiler). -Sitegeist.Monocle uses dummy data that is read directly from an annotation within the component code. That data ends up being a plain PHP array, that does not implement the desired interface. The PresentationObject enforcement would thus break Sitegeist.Monocle's component preview. +## Drawbacks -When the flag `isInPreviewMode` ist set to `true`, the default `props` context -is folded into the `presentationObject` context and the PresentationObject enforcement is deactivated. +### A step away from [Neos.Fusion](https://docs.neos.io/cms/manual/rendering/fusion) -This allows seamless use with tools like Sitegeist.Monocle. +Fusion is a domain specific language that specializes on declarative rendering instructions. As a DSL, Fusion is able to enforce a certain mindset linguistically. -*`EXAMPLE: Root.fusion`* +PresentationObjects move the concern of content integration largely over to PHP. And while PHP is a multi-paradigm language that *can* be used similarly to Fusion, it doesn't enforce that use at all. -```fusion -prototype(PackageFactory.AtomicFusion.PresentationObjects:PresentationObjectComponent) { - isInPreviewMode = ${request.controllerPackageKey == 'Sitegeist.Monocle'} -} -``` +So when using `PackageFactory.AtomicFusion.PresentationObjects`, you need to pay attention on your language use and avoid common anti-patterns. It is strongly recommended that you adhere closely to the value object pattern when writing PresentationObjects. For factories, familiarity with general PHP best practices is helpful (see for instance: https://phptherightway.com/). + +> **Hint:** PHP 8 will be released soon and comes with a lot of great language features that are going to allow to write most of the patterns presented here in a much more concise fashion. Especially noteworthy are [Constructor property promotion](https://wiki.php.net/rfc/constructor_promotion) and [Named arguments](https://wiki.php.net/rfc/named_params). For more on that, have a look at this article: https://stitcher.io/blog/new-in-php-8 + +### Verbosity + +PresentationObjects require you to write more code than plain AtomicFusion. To remedy that, this package comes with a [scaffolding tool](./Documentation/Kickstarter.md) to ease the creation of initial code structures. + +Currently, there's also a lot of concepts involved that spread information over the Codebase (`Classes/Presentation/`, `Resources/Private/Fusion/`, `Configuration/`), thus breaking the principle of co-location. + +In theory, co-location could be achieved by leveraging the `autoload.psr-4` configuration in the composer manifest (see: https://getcomposer.org/doc/04-schema.md#psr-4). However, the viability of this idea has not been proven yet. + +### Fusion Interoperation + +As of right now, Fusion is still the entry point for content integration. It's important to be aware of that, especially when changing factory method signatures, because this is a giant surface on which type-safety is lost. + +Fusion is required to handle two major concerns: + +1. **The internal content mapping and augmentation logic of Neos CMS.** Neos uses Fusion to map content repository nodes to their respective rendering instructions. It also uses Fusion to augment rendered content elements with information required by the Neos UI for inline editing. +2. **Content cache and partial page rendering.** Fusion provides the `@cache` annotation to enable individual caching instructions for different rendering paths. Its `ContentCache` service is able to resolve nested cached and uncached page fragments, thus allowing for maximum flexibility. -> The above example shows how `isInPreviewMode` can be set to true for all PresentationObjectComponents that are rendered in Sitegeist.Monocle. +Future developments of this package are going to focus on solutions for those two problems. ## License From 05fcb0e1c54492384a2c894ffa3efa1bd2cd4654 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Tue, 13 Oct 2020 22:13:56 +0200 Subject: [PATCH 27/28] Update composer version constraints --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 895c3f4..cd1f8db 100755 --- a/composer.json +++ b/composer.json @@ -11,8 +11,8 @@ } ], "require": { - "neos/neos": "~4.3", - "sitegeist/kaleidoscope": "^5.0" + "neos/neos": "^4.3 || dev-master", + "sitegeist/kaleidoscope": "^5.0 || dev-master" }, "require-dev": { "phpunit/phpunit": "^9.4", From e7677823ce13897c7d4b19416076ab54745f66d0 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Tue, 13 Oct 2020 22:14:22 +0200 Subject: [PATCH 28/28] Add Contribution section to README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index dd385dc..4ad6ebf 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,10 @@ Fusion is required to handle two major concerns: Future developments of this package are going to focus on solutions for those two problems. +## Contribution + +We will gladly accept contributions. Please send us pull requests. + ## License see [LICENSE](./LICENSE) \ No newline at end of file