diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c4450a156d..e962868ab0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4308,12 +4308,6 @@ parameters: count: 1 path: src/lib/Component/TwigComponent.php - - - message: '#^Offset ''meta'' might not exist on array\{position\: int, meta\?\: bool\}\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolver.php - - message: '#^Method Ibexa\\AdminUi\\Event\\Options\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -11687,12 +11681,6 @@ parameters: count: 1 path: src/lib/Service/ContentTypeService.php - - - message: '#^Access to an undefined property Ibexa\\Contracts\\Core\\Repository\\Values\\ValueObject\:\:\$fieldDefinitions\.$#' - identifier: property.notFound - count: 1 - path: src/lib/Service/MetaFieldType/MetaFieldDefinitionService.php - - message: '#^Access to an undefined property Ibexa\\Contracts\\Core\\Repository\\Values\\User\\Policy\:\:\$originalId\.$#' identifier: property.notFound @@ -14596,24 +14584,6 @@ parameters: count: 1 path: tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php - - - message: '#^Method Ibexa\\Tests\\AdminUi\\Config\\AdminUiForms\\ContentTypeFieldTypesResolverTest\:\:testGetMetaFieldTypes\(\) has parameter \$expectedMetaFieldTypes with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php - - - - message: ''' - #^PHPDoc tag @param has invalid value \(array\ - \$expectedMetaFieldTypes\)\: Unexpected token "\\n \* ", expected variable at offset 279 on line 11$# - ''' - identifier: phpDoc.parseError - count: 1 - path: tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php - - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with Symfony\\Component\\HttpFoundation\\Response will always evaluate to false\.$#' identifier: staticMethod.impossibleType diff --git a/src/bundle/DependencyInjection/Configuration/Parser/AdminUiForms.php b/src/bundle/DependencyInjection/Configuration/Parser/AdminUiForms.php index 70ea3fd2d9..75215998ab 100644 --- a/src/bundle/DependencyInjection/Configuration/Parser/AdminUiForms.php +++ b/src/bundle/DependencyInjection/Configuration/Parser/AdminUiForms.php @@ -133,6 +133,10 @@ static function (array $config): bool { ->defaultFalse() ->end() ->integerNode('position')->end() + ->booleanNode('unique') + ->info('Prevent this field_type from being added more than once per Content Type, regardless of the field group it already exists in') + ->defaultFalse() + ->end() ->end() ->end() ->end() diff --git a/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolver.php b/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolver.php index a5be5280e0..70d88a9742 100644 --- a/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolver.php +++ b/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolver.php @@ -27,6 +27,7 @@ public function __construct(ConfigResolverInterface $configResolver) * @return array */ public function getFieldTypes(): array @@ -43,7 +44,7 @@ public function getMetaFieldTypes(): array $fieldTypes = $this->getFieldTypes(); $metaFieldTypes = array_filter( $fieldTypes, - static fn (array $config): bool => true === $config['meta'] + static fn (array $config): bool => true === ($config['meta'] ?? false) ); $positions = array_column($metaFieldTypes, 'position'); diff --git a/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverInterface.php b/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverInterface.php index cce1704887..a934e04b2b 100644 --- a/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverInterface.php +++ b/src/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverInterface.php @@ -16,6 +16,7 @@ interface ContentTypeFieldTypesResolverInterface /** * @return array */ @@ -25,6 +26,7 @@ public function getFieldTypes(): array; * @return array */ public function getMetaFieldTypes(): array; diff --git a/src/lib/Service/MetaFieldType/MetaFieldDefinitionService.php b/src/lib/Service/MetaFieldType/MetaFieldDefinitionService.php index 06a10178df..a44fd7265f 100644 --- a/src/lib/Service/MetaFieldType/MetaFieldDefinitionService.php +++ b/src/lib/Service/MetaFieldType/MetaFieldDefinitionService.php @@ -60,6 +60,9 @@ public function __construct( $this->translator = $translator; } + /** + * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeCreateStruct|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeDraft $contentType + */ public function addMetaFieldDefinitions(ValueObject $contentType, ?Language $language = null): void { $metaFieldTypes = $this->contentTypeFieldTypesResolver->getMetaFieldTypes(); @@ -70,8 +73,9 @@ public function addMetaFieldDefinitions(ValueObject $contentType, ?Language $lan foreach ($metaFieldTypes as $metaFieldTypeIdentifier => $metaFieldTypeSettings) { $fieldGroup = $this->getDefaultMetaDataFieldTypeGroup() ?? $this->fieldsGroupsList->getDefaultGroup(); + $fieldTypeGroup = $metaFieldTypeSettings['unique'] ? null : $fieldGroup; - if ($this->metaFieldDefinitionExists($metaFieldTypeIdentifier, $fieldGroup, $contentType)) { + if ($this->metaFieldDefinitionExists($metaFieldTypeIdentifier, $fieldTypeGroup, $contentType)) { continue; } @@ -120,15 +124,18 @@ public function createMetaFieldDefinitionCreateStruct( return $fieldDefinitionCreateStruct; } + /** + * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeCreateStruct|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeDraft $contentType + */ public function metaFieldDefinitionExists( string $fieldTypeIdentifier, - string $fieldTypeGroup, + ?string $fieldTypeGroup, ValueObject $contentType ): bool { foreach ($contentType->fieldDefinitions as $fieldDefinition) { if ( $fieldDefinition->fieldTypeIdentifier === $fieldTypeIdentifier - && $fieldDefinition->fieldGroup === $fieldTypeGroup + && ($fieldTypeGroup === null || $fieldDefinition->fieldGroup === $fieldTypeGroup) ) { return true; } diff --git a/src/lib/Service/MetaFieldType/MetaFieldDefinitionServiceInterface.php b/src/lib/Service/MetaFieldType/MetaFieldDefinitionServiceInterface.php index 9728c865e0..e9e85a5e55 100644 --- a/src/lib/Service/MetaFieldType/MetaFieldDefinitionServiceInterface.php +++ b/src/lib/Service/MetaFieldType/MetaFieldDefinitionServiceInterface.php @@ -17,6 +17,9 @@ */ interface MetaFieldDefinitionServiceInterface { + /** + * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeCreateStruct|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeDraft $contentType + */ public function addMetaFieldDefinitions( ValueObject $contentType, ?Language $language = null @@ -29,9 +32,12 @@ public function createMetaFieldDefinitionCreateStruct( int $position ): FieldDefinitionCreateStruct; + /** + * @param \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeCreateStruct|\Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeDraft $contentType + */ public function metaFieldDefinitionExists( string $fieldTypeIdentifier, - string $fieldTypeGroup, + ?string $fieldTypeGroup, ValueObject $contentType ): bool; diff --git a/tests/bundle/DependencyInjection/Configuration/Parser/AdminUiFormsFieldTypesConfigurationTest.php b/tests/bundle/DependencyInjection/Configuration/Parser/AdminUiFormsFieldTypesConfigurationTest.php new file mode 100644 index 0000000000..c25451aab1 --- /dev/null +++ b/tests/bundle/DependencyInjection/Configuration/Parser/AdminUiFormsFieldTypesConfigurationTest.php @@ -0,0 +1,91 @@ +processor = new Processor(); + } + + public function testMetaAndUniqueDefaultToFalseWhenNotConfigured(): void + { + $result = $this->processFieldTypes([ + 'plain_field' => [], + ]); + + self::assertSame( + ['meta' => false, 'unique' => false], + $result['plain_field'] + ); + } + + public function testUniqueCanBeExplicitlyEnabledForAMetaFieldType(): void + { + $result = $this->processFieldTypes([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => true], + ]); + + self::assertSame( + ['meta' => true, 'position' => 1, 'unique' => true], + $result['seo_metadata'] + ); + } + + public function testPositionIsRequiredForMetaFieldTypes(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "position" option is required for all Meta Field Types'); + + $this->processFieldTypes([ + 'seo_metadata' => ['meta' => true], + ]); + } + + public function testPositionIsNotRequiredForNonMetaFieldTypes(): void + { + $result = $this->processFieldTypes([ + 'plain_field' => ['meta' => false], + ]); + + self::assertArrayNotHasKey('position', $result['plain_field']); + } + + /** + * @param array> $fieldTypes + * + * @return array> + */ + private function processFieldTypes(array $fieldTypes): array + { + $treeBuilder = new TreeBuilder('admin_ui_forms_test'); + $nodeBuilder = $treeBuilder->getRootNode()->children(); + (new AdminUiForms())->addSemanticConfig($nodeBuilder); + + $config = $this->processor->process($treeBuilder->buildTree(), [ + [ + 'admin_ui_forms' => [ + 'content_type_edit' => [ + 'field_types' => $fieldTypes, + ], + ], + ], + ]); + + return $config['admin_ui_forms']['content_type_edit']['field_types']; + } +} diff --git a/tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php b/tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php index 177510df80..57bf26ddda 100644 --- a/tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php +++ b/tests/lib/Config/AdminUiForms/ContentTypeFieldTypesResolverTest.php @@ -56,12 +56,13 @@ public function testGetFieldTypes(bool $hasParameter, array $expectedFieldTypes) * @param array $fieldTypes * @param array - * $expectedMetaFieldTypes + * 'unique': bool, + * }> $expectedMetaFieldTypes */ public function testGetMetaFieldTypes( bool $hasParameter, @@ -83,6 +84,7 @@ public function testGetMetaFieldTypes( * @param array $metaFieldTypes * @param array $expectedIdentifiers */ @@ -138,10 +140,12 @@ public function provideDataForTestGetFieldTypes(): iterable * array, * array * }> */ @@ -156,11 +160,13 @@ public function provideDataForTestGetMetaFieldTypes(): iterable $foo = [ 'meta' => true, 'position' => 2, + 'unique' => false, ]; $bar = [ 'meta' => true, 'position' => 10, + 'unique' => true, ]; yield [ @@ -185,6 +191,7 @@ public function provideDataForTestGetMetaFieldTypes(): iterable * array, * array * }> @@ -203,10 +210,12 @@ public function provideDataForTestGetMetaFieldTypeIdentifiers(): iterable 'foo' => [ 'meta' => true, 'position' => 1, + 'unique' => false, ], 'bar' => [ 'meta' => true, 'position' => 2, + 'unique' => false, ], ], ['foo', 'bar'], diff --git a/tests/lib/Service/MetaFieldType/MetaFieldDefinitionServiceTest.php b/tests/lib/Service/MetaFieldType/MetaFieldDefinitionServiceTest.php new file mode 100644 index 0000000000..f456a0ac76 --- /dev/null +++ b/tests/lib/Service/MetaFieldType/MetaFieldDefinitionServiceTest.php @@ -0,0 +1,352 @@ +configResolver = $this->createMock(ConfigResolverInterface::class); + $this->contentTypeFieldTypesResolver = $this->createMock(ContentTypeFieldTypesResolverInterface::class); + $this->contentTypeService = $this->createMock(ContentTypeService::class); + $this->fieldsGroupsList = $this->createMock(FieldsGroupsList::class); + $this->languageService = $this->createMock(LanguageService::class); + $this->localeConverter = $this->createMock(LocaleConverterInterface::class); + $this->translator = $this->createMock(TranslatorInterface::class); + + $this->fieldsGroupsList->method('getDefaultGroup')->willReturn(self::DEFAULT_GROUP); + + $this->metaFieldDefinitionService = new MetaFieldDefinitionService( + $this->configResolver, + $this->contentTypeFieldTypesResolver, + $this->contentTypeService, + $this->fieldsGroupsList, + $this->languageService, + $this->localeConverter, + $this->translator + ); + } + + public function testAddMetaFieldDefinitionsAddsMissingMetaFieldToContentTypeCreateStruct(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => false], + ]); + $this->translator->method('trans')->willReturn('SEO Metadata'); + $this->localeConverter->method('convertToPOSIX')->willReturn('eng_GB'); + + $contentType = new ContentTypeCreateStruct(); + $contentType->fieldDefinitions = []; + + $this->mockNewFieldDefinitionCreateStruct(); + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType, $this->createLanguage()); + + self::assertCount(1, $contentType->fieldDefinitions); + self::assertSame('seo_metadata', $contentType->fieldDefinitions[0]->fieldTypeIdentifier); + self::assertSame(self::DEFAULT_GROUP, $contentType->fieldDefinitions[0]->fieldGroup); + } + + public function testAddMetaFieldDefinitionsSkipsExistingNonUniqueFieldInSameGroup(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => false], + ]); + + $contentType = new ContentTypeCreateStruct(); + $existing = new FieldDefinitionCreateStruct(); + $existing->fieldTypeIdentifier = 'seo_metadata'; + $existing->fieldGroup = self::DEFAULT_GROUP; + $contentType->fieldDefinitions = [$existing]; + + $this->contentTypeService->expects(self::never())->method('newFieldDefinitionCreateStruct'); + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType, $this->createLanguage()); + + self::assertCount(1, $contentType->fieldDefinitions); + } + + public function testAddMetaFieldDefinitionsSkipsExistingUniqueFieldRegardlessOfGroup(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => true], + ]); + + $contentType = new ContentTypeCreateStruct(); + $existing = new FieldDefinitionCreateStruct(); + $existing->fieldTypeIdentifier = 'seo_metadata'; + $existing->fieldGroup = 'a_completely_different_group'; + $contentType->fieldDefinitions = [$existing]; + + $this->contentTypeService->expects(self::never())->method('newFieldDefinitionCreateStruct'); + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType, $this->createLanguage()); + + self::assertCount(1, $contentType->fieldDefinitions); + } + + public function testAddMetaFieldDefinitionsAddsDuplicateNonUniqueFieldWhenInDifferentGroup(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => false], + ]); + $this->translator->method('trans')->willReturn('SEO Metadata'); + $this->localeConverter->method('convertToPOSIX')->willReturn('eng_GB'); + $this->mockNewFieldDefinitionCreateStruct(); + + $contentType = new ContentTypeCreateStruct(); + $existing = new FieldDefinitionCreateStruct(); + $existing->fieldTypeIdentifier = 'seo_metadata'; + $existing->fieldGroup = 'a_completely_different_group'; + $contentType->fieldDefinitions = [$existing]; + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType, $this->createLanguage()); + + self::assertCount(2, $contentType->fieldDefinitions); + } + + public function testAddMetaFieldDefinitionsUsesContentTypeServiceForContentTypeDraft(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([ + 'seo_metadata' => ['meta' => true, 'position' => 1, 'unique' => false], + ]); + $this->translator->method('trans')->willReturn('SEO Metadata'); + $this->localeConverter->method('convertToPOSIX')->willReturn('eng_GB'); + $this->mockNewFieldDefinitionCreateStruct(); + + $contentType = $this->createContentTypeDraft([]); + + $this->contentTypeService->expects(self::once())->method('addFieldDefinition') + ->with($contentType, self::isInstanceOf(FieldDefinitionCreateStruct::class)); + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType, $this->createLanguage()); + } + + public function testAddMetaFieldDefinitionsUsesDefaultLanguageWhenNoneGiven(): void + { + $this->mockNoConfiguredDefaultMetaFieldTypeGroup(); + $this->contentTypeFieldTypesResolver->method('getMetaFieldTypes')->willReturn([]); + + $this->languageService->expects(self::once()) + ->method('getDefaultLanguageCode') + ->willReturn(self::LANGUAGE_CODE); + $this->languageService->expects(self::once()) + ->method('loadLanguage') + ->with(self::LANGUAGE_CODE) + ->willReturn($this->createLanguage()); + + $contentType = new ContentTypeCreateStruct(); + $contentType->fieldDefinitions = []; + + $this->metaFieldDefinitionService->addMetaFieldDefinitions($contentType); + } + + public function testMetaFieldDefinitionExistsMatchesByIdentifierWhenGroupIsNull(): void + { + $contentType = $this->createContentTypeDraft([ + $this->createFieldDefinition('seo_metadata', 'other_group'), + ]); + + self::assertTrue( + $this->metaFieldDefinitionService->metaFieldDefinitionExists('seo_metadata', null, $contentType) + ); + } + + public function testMetaFieldDefinitionExistsRequiresMatchingGroupWhenGiven(): void + { + $contentType = $this->createContentTypeDraft([ + $this->createFieldDefinition('seo_metadata', 'other_group'), + ]); + + self::assertFalse( + $this->metaFieldDefinitionService->metaFieldDefinitionExists('seo_metadata', self::DEFAULT_GROUP, $contentType) + ); + self::assertTrue( + $this->metaFieldDefinitionService->metaFieldDefinitionExists('seo_metadata', 'other_group', $contentType) + ); + } + + public function testMetaFieldDefinitionExistsReturnsFalseWhenIdentifierIsMissing(): void + { + $contentType = $this->createContentTypeDraft([ + $this->createFieldDefinition('seo_metadata', self::DEFAULT_GROUP), + ]); + + self::assertFalse( + $this->metaFieldDefinitionService->metaFieldDefinitionExists('other_field_type', null, $contentType) + ); + } + + public function testCreateMetaFieldDefinitionCreateStruct(): void + { + $fieldDefinitionCreateStruct = new FieldDefinitionCreateStruct(); + $this->contentTypeService->expects(self::once()) + ->method('newFieldDefinitionCreateStruct') + ->with(self::isType('string'), 'seo_metadata') + ->willReturn($fieldDefinitionCreateStruct); + + $this->translator->expects(self::once()) + ->method('trans') + ->with('seo_metadata.name', [], 'ibexa_fieldtypes', 'eng_GB') + ->willReturn('SEO Metadata'); + + $this->localeConverter->expects(self::once()) + ->method('convertToPOSIX') + ->with(self::LANGUAGE_CODE) + ->willReturn('eng_GB'); + + $result = $this->metaFieldDefinitionService->createMetaFieldDefinitionCreateStruct( + 'seo_metadata', + self::DEFAULT_GROUP, + $this->createLanguage(), + 3 + ); + + self::assertSame(self::DEFAULT_GROUP, $result->fieldGroup); + self::assertSame(3, $result->position); + self::assertSame([self::LANGUAGE_CODE => 'SEO Metadata'], $result->names); + } + + public function testGetDefaultMetaDataFieldTypeGroupReturnsNullWhenParameterNotSet(): void + { + $this->configResolver->expects(self::once()) + ->method('hasParameter') + ->with(AdminUiForms::CONTENT_TYPE_DEFAULT_META_FIELD_TYPE_GROUP_PARAM) + ->willReturn(false); + $this->configResolver->expects(self::never())->method('getParameter'); + + self::assertNull($this->metaFieldDefinitionService->getDefaultMetaDataFieldTypeGroup()); + } + + public function testGetDefaultMetaDataFieldTypeGroupReturnsConfiguredValue(): void + { + $this->configResolver->expects(self::once()) + ->method('hasParameter') + ->with(AdminUiForms::CONTENT_TYPE_DEFAULT_META_FIELD_TYPE_GROUP_PARAM) + ->willReturn(true); + $this->configResolver->expects(self::once()) + ->method('getParameter') + ->with(AdminUiForms::CONTENT_TYPE_DEFAULT_META_FIELD_TYPE_GROUP_PARAM) + ->willReturn('custom_group'); + + self::assertSame( + 'custom_group', + $this->metaFieldDefinitionService->getDefaultMetaDataFieldTypeGroup() + ); + } + + private function mockNoConfiguredDefaultMetaFieldTypeGroup(): void + { + $this->configResolver->method('hasParameter') + ->with(AdminUiForms::CONTENT_TYPE_DEFAULT_META_FIELD_TYPE_GROUP_PARAM) + ->willReturn(false); + } + + private function createLanguage(): Language + { + return new Language(['languageCode' => self::LANGUAGE_CODE]); + } + + private function createFieldDefinition(string $fieldTypeIdentifier, string $fieldGroup): FieldDefinition + { + return new FieldDefinition([ + 'identifier' => $fieldTypeIdentifier, + 'fieldTypeIdentifier' => $fieldTypeIdentifier, + 'fieldGroup' => $fieldGroup, + ]); + } + + /** + * @param array<\Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition> $fieldDefinitions + */ + private function createContentTypeDraft(array $fieldDefinitions): ContentTypeDraft + { + $fieldDefinitionCollection = new FieldDefinitionCollection($fieldDefinitions); + + /** @var \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentTypeDraft|\PHPUnit\Framework\MockObject\MockObject $contentTypeDraft */ + $contentTypeDraft = $this->getMockForAbstractClass( + ContentTypeDraft::class, + [], + '', + true, + true, + true, + ['getFieldDefinitions', '__get'] + ); + $contentTypeDraft->method('getFieldDefinitions')->willReturn($fieldDefinitionCollection); + $contentTypeDraft->method('__get')->willReturnCallback( + static fn (string $property): ?FieldDefinitionCollection => 'fieldDefinitions' === $property ? $fieldDefinitionCollection : null + ); + + return $contentTypeDraft; + } + + private function mockNewFieldDefinitionCreateStruct(): void + { + $this->contentTypeService->method('newFieldDefinitionCreateStruct') + ->willReturnCallback(static function (string $identifier, string $fieldTypeIdentifier): FieldDefinitionCreateStruct { + $fieldDefinitionCreateStruct = new FieldDefinitionCreateStruct(); + $fieldDefinitionCreateStruct->identifier = $identifier; + $fieldDefinitionCreateStruct->fieldTypeIdentifier = $fieldTypeIdentifier; + + return $fieldDefinitionCreateStruct; + }); + } +}