diff --git a/Classes/Aspect/BreadcrumbListAspect.php b/Classes/Aspect/BreadcrumbListAspect.php index d2d22bac..39c59d62 100644 --- a/Classes/Aspect/BreadcrumbListAspect.php +++ b/Classes/Aspect/BreadcrumbListAspect.php @@ -13,7 +13,7 @@ use Brotkrueml\Schema\Core\Model\AbstractType; use Brotkrueml\Schema\Manager\SchemaManager; use Brotkrueml\Schema\Model\Type; -use Brotkrueml\Schema\Utility\Utility; +use Brotkrueml\Schema\Provider\TypesProvider; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -27,17 +27,22 @@ final class BreadcrumbListAspect implements AspectInterface /** @var ExtensionConfiguration */ private $configuration; - /** @var object|ContentObjectRenderer */ + /** @var ContentObjectRenderer */ private $contentObjectRenderer; + /** @var TypesProvider */ + private $typesProvider; + public function __construct( TypoScriptFrontendController $controller = null, ExtensionConfiguration $configuration = null, - ContentObjectRenderer $contentObjectRenderer = null + ContentObjectRenderer $contentObjectRenderer = null, + TypesProvider $typesProvider = null ) { - $this->controller = $controller ?: $GLOBALS['TSFE']; - $this->configuration = $configuration ?: GeneralUtility::makeInstance(ExtensionConfiguration::class); - $this->contentObjectRenderer = $contentObjectRenderer ?: GeneralUtility::makeInstance(ContentObjectRenderer::class); + $this->controller = $controller ?? $GLOBALS['TSFE']; + $this->configuration = $configuration ?? GeneralUtility::makeInstance(ExtensionConfiguration::class); + $this->contentObjectRenderer = $contentObjectRenderer ?? GeneralUtility::makeInstance(ContentObjectRenderer::class); + $this->typesProvider = $typesProvider ?? new TypesProvider(); } public function execute(SchemaManager $schemaManager): void @@ -77,8 +82,8 @@ private function buildBreadCrumbList(array $rootLine): Type\BreadcrumbList { $breadcrumbList = (new Type\BreadcrumbList()); foreach ($rootLine as $index => $page) { - $givenItemTypeClass = Utility::getNamespacedClassNameForType($page['tx_schema_webpagetype']); - $webPageTypeClass = $givenItemTypeClass ?: Type\WebPage::class; + $givenItemTypeClass = $this->typesProvider->resolveTypeToModel($page['tx_schema_webpagetype']); + $webPageTypeClass = $givenItemTypeClass ?? Type\WebPage::class; /** @var AbstractType $itemType */ $itemType = new $webPageTypeClass(); diff --git a/Classes/Aspect/WebPageAspect.php b/Classes/Aspect/WebPageAspect.php index b1da3670..eb2cae0c 100644 --- a/Classes/Aspect/WebPageAspect.php +++ b/Classes/Aspect/WebPageAspect.php @@ -12,7 +12,7 @@ use Brotkrueml\Schema\Core\Model\AbstractType; use Brotkrueml\Schema\Manager\SchemaManager; -use Brotkrueml\Schema\Utility\Utility; +use Brotkrueml\Schema\Provider\TypesProvider; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -27,12 +27,17 @@ final class WebPageAspect implements AspectInterface /** @var ExtensionConfiguration */ private $configuration; + /** @var TypesProvider */ + private $typesProvider; + public function __construct( TypoScriptFrontendController $controller = null, - ExtensionConfiguration $configuration = null + ExtensionConfiguration $configuration = null, + TypesProvider $typesProvider = null ) { - $this->controller = $controller ?: $GLOBALS['TSFE']; - $this->configuration = $configuration ?: GeneralUtility::makeInstance(ExtensionConfiguration::class); + $this->controller = $controller ?? $GLOBALS['TSFE']; + $this->configuration = $configuration ?? GeneralUtility::makeInstance(ExtensionConfiguration::class); + $this->typesProvider = $typesProvider ?? new TypesProvider(); } public function execute(SchemaManager $schemaManager): void @@ -50,10 +55,10 @@ public function execute(SchemaManager $schemaManager): void $type = $this->controller->page['tx_schema_webpagetype'] ?: static::DEFAULT_WEBPAGE_TYPE; - $webPageClass = Utility::getNamespacedClassNameForType($type); + $webPageClass = $this->typesProvider->resolveTypeToModel($type); if ($webPageClass) { /** @var AbstractType $webPage */ - $webPage = GeneralUtility::makeInstance($webPageClass); + $webPage = new $webPageClass(); if ($this->controller->page['endtime']) { $webPage->setProperty('expires', \date('c', $this->controller->page['endtime'])); diff --git a/Classes/Provider/TypesProvider.php b/Classes/Provider/TypesProvider.php index ceddc1d1..e2558ccb 100644 --- a/Classes/Provider/TypesProvider.php +++ b/Classes/Provider/TypesProvider.php @@ -184,4 +184,20 @@ public function getContentTypes(): array ) ); } + + /** + * @internal Only for internal use, not a public API! + */ + public function resolveTypeToModel(string $type): ?string + { + if (empty(static::$types)) { + $this->getTypesWithModels(); + } + + if (\array_key_exists($type, static::$types)) { + return static::$types[$type]; + } + + return null; + } } diff --git a/Classes/Utility/Utility.php b/Classes/Utility/Utility.php deleted file mode 100644 index 32c9a3cd..00000000 --- a/Classes/Utility/Utility.php +++ /dev/null @@ -1,52 +0,0 @@ -resolveTypeToModel($arguments[static::ARGUMENT_BREADCRUMB][$i]['data']['tx_schema_webpagetype']); $webPageTypeClass = $givenItemTypeClass ?: $webPageTypeClass; } diff --git a/Tests/Unit/Aspect/BreadcrumbListAspectTest.php b/Tests/Unit/Aspect/BreadcrumbListAspectTest.php index 5ed19942..c4fcb89b 100644 --- a/Tests/Unit/Aspect/BreadcrumbListAspectTest.php +++ b/Tests/Unit/Aspect/BreadcrumbListAspectTest.php @@ -5,9 +5,11 @@ use Brotkrueml\Schema\Aspect\BreadcrumbListAspect; use Brotkrueml\Schema\Manager\SchemaManager; +use Brotkrueml\Schema\Provider\TypesProvider; +use Brotkrueml\Schema\Tests\Fixtures\Model\Type\ItemPage; use Brotkrueml\Schema\Tests\Helper\SchemaCacheTrait; -use Brotkrueml\Schema\Tests\Unit\Helper\TypeFixtureNamespace; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; @@ -20,7 +22,6 @@ class BreadcrumbListAspectTest extends UnitTestCase { use SchemaCacheTrait; - use TypeFixtureNamespace; protected $resetSingletonInstances = true; @@ -36,6 +37,9 @@ class BreadcrumbListAspectTest extends UnitTestCase /** @var MockObject|ContentObjectRenderer */ protected $contentObjectRendererMock; + /** @var Stub|TypesProvider */ + private $typesProviderStub; + protected function setUp(): void { $this->defineCacheStubsWhichReturnEmptyEntry(); @@ -70,7 +74,8 @@ public function automaticBreadcrumbListGenerationIsDeactivated(): void (new BreadcrumbListAspect( $this->controllerMock, $configurationMock, - $this->contentObjectRendererMock + $this->contentObjectRendererMock, + $this->typesProviderStub )) ->execute($schemaManagerMock); } @@ -79,6 +84,7 @@ protected function setUpGeneralMocks(): void { $this->controllerMock = $this->createMock(TypoScriptFrontendController::class); $this->contentObjectRendererMock = $this->createMock(ContentObjectRenderer::class); + $this->typesProviderStub = $this->createStub(TypesProvider::class); } /** @@ -99,7 +105,8 @@ public function withActivatedConfigurationOptionAndEmptyRootlineNoMarkupIsGenera (new BreadcrumbListAspect( $this->controllerMock, $this->getExtensionConfigurationMockWithGetReturnsTrue(), - $this->contentObjectRendererMock + $this->contentObjectRendererMock, + $this->typesProviderStub )) ->execute($schemaManagerMock); } @@ -121,30 +128,6 @@ private function getExtensionConfigurationMockWithGetReturnsTrue() public function rootLineProvider(): iterable { - yield 'Rootline with web page type set' => [ - [ - 2 => [ - 'uid' => 2, - 'doktype' => PageRepository::DOKTYPE_DEFAULT, - 'title' => 'A page', - 'nav_title' => '', - 'nav_hide' => '0', - 'is_siteroot' => '0', - 'tx_schema_webpagetype' => 'ItemPage', - ], - 1 => [ - 'uid' => 1, - 'doktype' => PageRepository::DOKTYPE_DEFAULT, - 'title' => 'Site root page', - 'nav_title' => '', - 'nav_hide' => '0', - 'is_siteroot' => '1', - 'tx_schema_webpagetype' => '', - ], - ], - '', - ]; - yield 'Rootline with nav_title set' => [ [ 2 => [ @@ -329,7 +312,8 @@ public function breadCrumbIsGeneratedCorrectly(array $rootLine, string $expected $subject = new BreadcrumbListAspect( $this->controllerMock, $this->getExtensionConfigurationMockWithGetReturnsTrue(), - $this->contentObjectRendererMock + $this->contentObjectRendererMock, + $this->typesProviderStub ); $subject->execute($schemaManager); @@ -380,7 +364,7 @@ public function breadCrumbIsSortedCorrectly(): void ]) ->willReturn('https://example.org/level-4/'); - $this->controllerMock->rootLine = [ + $this->controllerMock->rootLine = [ [ 'uid' => 111, 'doktype' => PageRepository::DOKTYPE_DEFAULT, @@ -433,11 +417,73 @@ public function breadCrumbIsSortedCorrectly(): void $subject = new BreadcrumbListAspect( $this->controllerMock, $this->getExtensionConfigurationMockWithGetReturnsTrue(), - $this->contentObjectRendererMock + $this->contentObjectRendererMock, + $this->typesProviderStub + ); + + $subject->execute($schemaManager); + + self::assertSame( + '', + $schemaManager->renderJsonLd() + ); + } + + /** + * @test + */ + public function rootlineWithDifferentWebPageTypeSet(): void + { + $this->setUpGeneralMocks(); + + $this->typesProviderStub + ->method('resolveTypeToModel') + ->with('ItemPage') + ->willReturn(ItemPage::class); + + $this->controllerMock->rootLine = [ + 2 => [ + 'uid' => 2, + 'doktype' => PageRepository::DOKTYPE_DEFAULT, + 'title' => 'A page', + 'nav_title' => '', + 'nav_hide' => '0', + 'is_siteroot' => '0', + 'tx_schema_webpagetype' => 'ItemPage', + ], + 1 => [ + 'uid' => 1, + 'doktype' => PageRepository::DOKTYPE_DEFAULT, + 'title' => 'Site root page', + 'nav_title' => '', + 'nav_hide' => '0', + 'is_siteroot' => '1', + 'tx_schema_webpagetype' => '', + ], + ]; + + $schemaManager = GeneralUtility::makeInstance(SchemaManager::class); + + $this->contentObjectRendererMock + ->expects(self::once()) + ->method('typoLink_URL') + ->with([ + 'parameter' => '2', + 'forceAbsoluteUrl' => true, + ]) + ->willReturn('https://example.org/the-page/'); + + $subject = new BreadcrumbListAspect( + $this->controllerMock, + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->contentObjectRendererMock, + $this->typesProviderStub ); $subject->execute($schemaManager); - self::assertSame('', $schemaManager->renderJsonLd()); + $expected = ''; + + self::assertSame($expected, $schemaManager->renderJsonLd()); } } diff --git a/Tests/Unit/Aspect/WebPageAspectTest.php b/Tests/Unit/Aspect/WebPageAspectTest.php index 7c7514d6..5cc428c6 100644 --- a/Tests/Unit/Aspect/WebPageAspectTest.php +++ b/Tests/Unit/Aspect/WebPageAspectTest.php @@ -5,22 +5,25 @@ use Brotkrueml\Schema\Aspect\WebPageAspect; use Brotkrueml\Schema\Manager\SchemaManager; +use Brotkrueml\Schema\Provider\TypesProvider; use Brotkrueml\Schema\Tests\Fixtures\Model\Type\ItemPage; use Brotkrueml\Schema\Tests\Fixtures\Model\Type\WebPage; use Brotkrueml\Schema\Tests\Helper\SchemaCacheTrait; -use Brotkrueml\Schema\Tests\Unit\Helper\TypeFixtureNamespace; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; class WebPageAspectTest extends TestCase { use SchemaCacheTrait; - use TypeFixtureNamespace; protected $resetSingletonInstances = true; @@ -33,15 +36,8 @@ class WebPageAspectTest extends TestCase /** @var MockObject|RequestHandlerInterface */ protected $handlerMock; - public static function setUpBeforeClass(): void - { - static::setTypeNamespaceToFixtureNamespace(); - } - - public static function tearDownAfterClass(): void - { - static::restoreOriginalTypeNamespace(); - } + /** @var Stub|TypesProvider */ + private $typesProviderStub; protected function setUp(): void { @@ -72,10 +68,34 @@ public function constructWorksCorrectlyWithNoParametersAreGiven(): void $configuration = $reflector->getProperty('configuration'); $configuration->setAccessible(true); + $typesProvider = $reflector->getProperty('typesProvider'); + $typesProvider->setAccessible(true); + + $packageManagerStub = $this->createStub(PackageManager::class); + $packageManagerStub + ->method('getActivePackages') + ->willReturn([]); + + GeneralUtility::setSingletonInstance(PackageManager::class, $packageManagerStub); + + $cacheFrontendStub = $this->createStub(FrontendInterface::class); + $cacheFrontendStub + ->method('get') + ->willReturn([]); + + $cacheManagerStub = $this->createStub(CacheManager::class); + $cacheManagerStub + ->method('getCache') + ->with('tx_schema_core') + ->willReturn($cacheFrontendStub); + + GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerStub); + $subject = new WebPageAspect(); self::assertSame('fake controller', $controller->getValue($subject)); self::assertInstanceOf(ExtensionConfiguration::class, $configuration->getValue($subject)); + self::assertInstanceOf(TypesProvider::class, $typesProvider->getValue($subject)); unset($GLOBALS['TSFE']); } @@ -103,13 +123,18 @@ public function whenAutomaticWebPageGenerationIsDeactivatedNoTypeIsAdded() ->expects(self::never()) ->method('hasWebPage'); - (new WebPageAspect($this->controllerMock, $configurationMock)) + (new WebPageAspect( + $this->controllerMock, + $configurationMock, + $this->typesProviderStub + )) ->execute($schemaManagerMock); } protected function setUpGeneralMocks(): void { $this->controllerMock = $this->createMock(TypoScriptFrontendController::class); + $this->typesProviderStub = $this->createStub(TypesProvider::class); } /** @@ -130,7 +155,8 @@ public function withAssignedWebPageModelRequestIsDirectlyPassedOverToNextMiddlew (new WebPageAspect( $this->controllerMock, - $this->getExtensionConfigurationMockWithGetReturnsTrue() + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->typesProviderStub )) ->execute($schemaManagerMock); } @@ -165,6 +191,11 @@ public function pagePropertyForWebPageTypeIsEmptyThenWebPageIsUsed(): void 'endtime' => 0, ]; + $this->typesProviderStub + ->method('resolveTypeToModel') + ->with('WebPage') + ->willReturn(WebPage::class); + /** @var MockObject|SchemaManager $schemaManagerMock */ $schemaManagerMock = $this->createMock(SchemaManager::class); $schemaManagerMock @@ -174,7 +205,8 @@ public function pagePropertyForWebPageTypeIsEmptyThenWebPageIsUsed(): void $subject = new WebPageAspect( $this->controllerMock, - $this->getExtensionConfigurationMockWithGetReturnsTrue() + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->typesProviderStub ); $subject->execute($schemaManagerMock); @@ -195,6 +227,11 @@ public function pagePropertyForWebPageTypeIsSetThenThisTypeIsUsed(): void 'endtime' => 0, ]; + $this->typesProviderStub + ->method('resolveTypeToModel') + ->with('ItemPage') + ->willReturn(ItemPage::class); + /** @var MockObject|SchemaManager $schemaManagerMock */ $schemaManagerMock = $this->createMock(SchemaManager::class); $schemaManagerMock @@ -204,7 +241,8 @@ public function pagePropertyForWebPageTypeIsSetThenThisTypeIsUsed(): void $subject = new WebPageAspect( $this->controllerMock, - $this->getExtensionConfigurationMockWithGetReturnsTrue() + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->typesProviderStub ); $subject->execute($schemaManagerMock); @@ -225,6 +263,11 @@ public function pagePropertyForEndtimeIsSetThenExpiresPropertyIsSet(): void 'endtime' => 1561672753, ]; + $this->typesProviderStub + ->method('resolveTypeToModel') + ->with('WebPage') + ->willReturn(WebPage::class); + /** @var MockObject|SchemaManager $schemaManagerMock */ $schemaManagerMock = $this->createMock(SchemaManager::class); $schemaManagerMock @@ -234,7 +277,8 @@ public function pagePropertyForEndtimeIsSetThenExpiresPropertyIsSet(): void $subject = new WebPageAspect( $this->controllerMock, - $this->getExtensionConfigurationMockWithGetReturnsTrue() + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->typesProviderStub ); $subject->execute($schemaManagerMock); @@ -261,7 +305,8 @@ public function whenTypeDoesNotExistsNoWebPageIsSet(): void (new WebPageAspect( $this->controllerMock, - $this->getExtensionConfigurationMockWithGetReturnsTrue() + $this->getExtensionConfigurationMockWithGetReturnsTrue(), + $this->typesProviderStub )) ->execute($schemaManagerMock); } diff --git a/Tests/Unit/Helper/TypeFixtureNamespace.php b/Tests/Unit/Helper/TypeFixtureNamespace.php deleted file mode 100644 index 52991584..00000000 --- a/Tests/Unit/Helper/TypeFixtureNamespace.php +++ /dev/null @@ -1,23 +0,0 @@ -cacheFrontendMock + ->method('has') + ->willReturn(false); + + $actual = $this->subject->resolveTypeToModel('FixtureImage'); + + self::assertSame(FixtureImage::class, $actual); + } + + /** + * @test + */ + public function resolveTypeToModelReturnsNullWhenTypeNotAvailable(): void + { + $this->cacheFrontendMock + ->method('has') + ->willReturn(false); + + $actual = $this->subject->resolveTypeToModel('NotConfiguredType'); + + self::assertNull($actual); + } } diff --git a/Tests/Unit/Utility/UtilityTest.php b/Tests/Unit/Utility/UtilityTest.php deleted file mode 100644 index 70a79095..00000000 --- a/Tests/Unit/Utility/UtilityTest.php +++ /dev/null @@ -1,42 +0,0 @@ -{"@context":"http://schema.org","@type":"BreadcrumbList","itemListElement":{"@type":"ListItem","item":{"@type":"WebPage","@id":"https://example.org/sub-page/"},"name":"Some sub page","position":"1"}}', ]; - yield 'Breadcrumb with multiple pages and webpage types given' => [ - '', - [ - 'breadcrumb' => [ - [ - 'title' => 'A web page', - 'link' => '/', - 'data' => [ - 'tx_schema_webpagetype' => 'WebPage', - ], - ], - [ - 'title' => 'Video overview', - 'link' => '/videos/', - 'data' => [ - 'tx_schema_webpagetype' => 'VideoGallery', - ], - ], - [ - 'title' => 'Unicorns in TYPO3 land', - 'link' => '/videos/unicorns-in-typo3-land/', - 'data' => [ - 'tx_schema_webpagetype' => 'ItemPage', - ], - ], - ], - ], - '', - ]; - yield 'Breadcrumb with multiple pages and a class given as data item (which can happen when you add a virtual category page with a domain model to it)' => [ '', [ @@ -185,6 +147,75 @@ public function itBuildsSchemaCorrectlyOutOfViewHelpers(string $template, array self::assertSame($expected, $actual); } + /** + * @test + */ + public function breadcrumbWithMultiplePagesAndWebPageTypesGiven(): void + { + /** @noinspection PhpInternalEntityUsedInspection */ + GeneralUtility::setIndpEnv('TYPO3_SITE_URL', 'https://example.org/'); + + $cacheFrontendStub = $this->createStub(PhpFrontend::class); + $cacheFrontendStub + ->method('has') + ->willReturn(true); + $cacheFrontendStub + ->method('require') + ->willReturn([ + 'ItemPage' => ItemPage::class, + 'VideoGallery' => VideoGallery::class, + 'WebPage' => WebPage::class, + ]); + + $cacheManagerStub = $this->createStub(CacheManager::class); + $cacheManagerStub + ->method('getCache') + ->with(self::anything()) + ->willReturn($cacheFrontendStub); + + GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerStub); + + $packageManagerStub = $this->createStub(PackageManager::class); + $packageManagerStub + ->method('getActivePackages') + ->willReturn([]); + + GeneralUtility::setSingletonInstance(PackageManager::class, $packageManagerStub); + + $variables = [ + 'breadcrumb' => [ + [ + 'title' => 'A web page', + 'link' => '/', + 'data' => [ + 'tx_schema_webpagetype' => 'WebPage', + ], + ], + [ + 'title' => 'Video overview', + 'link' => '/videos/', + 'data' => [ + 'tx_schema_webpagetype' => 'VideoGallery', + ], + ], + [ + 'title' => 'Unicorns in TYPO3 land', + 'link' => '/videos/unicorns-in-typo3-land/', + 'data' => [ + 'tx_schema_webpagetype' => 'ItemPage', + ], + ], + ], + ]; + + $this->renderTemplate('', $variables); + + $actual = $this->schemaManager->renderJsonLd(); + $expected = ''; + + self::assertSame($expected, $actual); + } + /** * Data provider for some cases where exceptions are thrown when using the property view helper incorrectly *