diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md
index 5f5bc2f..8ac5f7a 100644
--- a/CHANGELOG_de-DE.md
+++ b/CHANGELOG_de-DE.md
@@ -1,3 +1,6 @@
+# 5.3.1
+- Behoben: Alle 8 Fehler bei der Ausführung von `dal:validate` unter Shopware 6.7.14 [#87](https://github.com/Werkstattl/OpenBlogware/issues/87)
+
# 5.3.0
- Behoben: Falsche Kriterien-Associations [#84](https://github.com/Werkstattl/OpenBlogware/pull/84)
- Behoben: Mehrere Fehler [#82](https://github.com/Werkstattl/OpenBlogware/pull/82)
diff --git a/CHANGELOG_en-GB.md b/CHANGELOG_en-GB.md
index e69b5e7..ace725e 100644
--- a/CHANGELOG_en-GB.md
+++ b/CHANGELOG_en-GB.md
@@ -1,3 +1,6 @@
+# 5.3.1
+- Fixed: all 8 errors reported by `dal:validate` on Shopware 6.7.14 [#87](https://github.com/Werkstattl/OpenBlogware/issues/87)
+
# 5.3.0
- Fixed: wrong criteria associations [#84](https://github.com/Werkstattl/OpenBlogware/pull/84)
- Fixed: several errors [#82](https://github.com/Werkstattl/OpenBlogware/pull/82)
diff --git a/src/Content/Blog/BlogEntryDefinition.php b/src/Content/Blog/BlogEntryDefinition.php
index 7c372cc..56062ae 100644
--- a/src/Content/Blog/BlogEntryDefinition.php
+++ b/src/Content/Blog/BlogEntryDefinition.php
@@ -17,13 +17,13 @@
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
-use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
use Shopware\Core\System\Tag\TagDefinition;
use Werkl\OpenBlogware\Content\Blog\Aggregate\BlogEntryBlogCategoryMappingDefinition;
+use Werkl\OpenBlogware\Content\Blog\Aggregate\BlogEntryTagMappingDefinition;
use Werkl\OpenBlogware\Content\Blog\BlogEntryTranslation\BlogEntryTranslationDefinition;
use Werkl\OpenBlogware\Content\BlogAuthor\BlogAuthorDefinition;
use Werkl\OpenBlogware\Content\BlogCategory\BlogCategoryDefinition;
@@ -61,7 +61,7 @@ protected function defineFields(): FieldCollection
(new FkField('author_id', 'authorId', BlogAuthorDefinition::class))->addFlags(new Required()),
(new FkField('cms_page_id', 'cmsPageId', CmsPageDefinition::class))->addFlags(new ApiAware(), new Inherited()),
- (new ReferenceVersionField(CmsPageDefinition::class))->addFlags(new PrimaryKey(), new Required()),
+ (new ReferenceVersionField(CmsPageDefinition::class))->addFlags(new Required(), new ApiAware()),
(new TranslatedField('mediaId'))->addFlags(new ApiAware()),
(new TranslatedField('title'))->addFlags(new ApiAware(), new SearchRanking(SearchRanking::HIGH_SEARCH_RANKING)),
@@ -78,8 +78,8 @@ protected function defineFields(): FieldCollection
(new ManyToManyAssociationField('blogCategories', BlogCategoryDefinition::class, BlogEntryBlogCategoryMappingDefinition::class, 'werkl_blog_entry_id', 'werkl_blog_category_id'))->addFlags(new CascadeDelete(), new ApiAware(), new SearchRanking(SearchRanking::ASSOCIATION_SEARCH_RANKING)),
(new ManyToOneAssociationField('blogAuthor', 'author_id', BlogAuthorDefinition::class, 'id', false))->addFlags(new ApiAware(), new SearchRanking(SearchRanking::ASSOCIATION_SEARCH_RANKING)),
- (new OneToOneAssociationField('cmsPage', 'cms_page_id', 'id', CmsPageDefinition::class, false))->addFlags(new ApiAware()),
- (new ManyToManyAssociationField('tags', TagDefinition::class, 'werkl_blog_entry_tag', 'werkl_blog_entry_id', 'tag_id'))->addFlags(new ApiAware()),
+ (new ManyToOneAssociationField('cmsPage', 'cms_page_id', CmsPageDefinition::class, 'id', false))->addFlags(new ApiAware()),
+ (new ManyToManyAssociationField('tags', TagDefinition::class, BlogEntryTagMappingDefinition::class, 'werkl_blog_entry_id', 'tag_id'))->addFlags(new ApiAware()),
]);
}
}
diff --git a/src/Content/Blog/BlogEntryEntity.php b/src/Content/Blog/BlogEntryEntity.php
index ee7b830..2862b10 100644
--- a/src/Content/Blog/BlogEntryEntity.php
+++ b/src/Content/Blog/BlogEntryEntity.php
@@ -44,6 +44,10 @@ class BlogEntryEntity extends Entity
protected ?string $mediaId = null;
+ /**
+ * @internal The teaser image is stored in the translations (mediaId), this association is
+ * hydrated manually by the BlogSubscriber and is not backed by a DAL field.
+ */
protected ?MediaEntity $media = null;
protected \DateTimeInterface $publishedAt;
@@ -241,7 +245,7 @@ public function getTags(): ?TagCollection
return $this->tags;
}
- public function setTags(?TagCollection $tags): void
+ public function setTags(TagCollection $tags): void
{
$this->tags = $tags;
}
diff --git a/src/Content/Extension/TagExtension.php b/src/Content/Extension/TagExtension.php
new file mode 100644
index 0000000..a47d815
--- /dev/null
+++ b/src/Content/Extension/TagExtension.php
@@ -0,0 +1,27 @@
+add(
+ (new ManyToManyAssociationField('blogEntries', BlogEntryDefinition::class, BlogEntryTagMappingDefinition::class, 'tag_id', 'werkl_blog_entry_id'))->addFlags(new CascadeDelete()),
+ );
+ }
+
+ public function getEntityName(): string
+ {
+ return TagDefinition::ENTITY_NAME;
+ }
+}
diff --git a/src/Migration/Migration1789100000NullableTranslationColumns.php b/src/Migration/Migration1789100000NullableTranslationColumns.php
new file mode 100644
index 0000000..4320f36
--- /dev/null
+++ b/src/Migration/Migration1789100000NullableTranslationColumns.php
@@ -0,0 +1,38 @@
+executeStatement('
+ ALTER TABLE `werkl_blog_entry_translation`
+ MODIFY COLUMN `title` VARCHAR(255) NULL,
+ MODIFY COLUMN `slug` VARCHAR(255) NULL;
+ ');
+
+ $connection->executeStatement('
+ ALTER TABLE `werkl_blog_category_translation`
+ MODIFY COLUMN `name` VARCHAR(255) NULL;
+ ');
+ }
+
+ public function updateDestructive(Connection $connection): void
+ {
+ // implement update destructive
+ }
+}
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index ee63aff..3505510 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -53,6 +53,10 @@
+
+
+
+
diff --git a/tests/unit/Content/Blog/BlogEntryDefinitionTest.php b/tests/unit/Content/Blog/BlogEntryDefinitionTest.php
new file mode 100644
index 0000000..add14bd
--- /dev/null
+++ b/tests/unit/Content/Blog/BlogEntryDefinitionTest.php
@@ -0,0 +1,171 @@
+
+ */
+ private array $fields;
+
+ protected function setUp(): void
+ {
+ $this->fields = $this->defineFieldsByName(new BlogEntryDefinition());
+ }
+
+ /**
+ * The table has PRIMARY KEY (id) only, so no other field may carry the PrimaryKey flag.
+ */
+ public function testOnlyIdFieldIsFlaggedAsPrimaryKey(): void
+ {
+ $propertyNames = [];
+
+ foreach ($this->fields as $propertyName => $field) {
+ if ($field->is(PrimaryKey::class)) {
+ $propertyNames[] = $propertyName;
+ }
+ }
+
+ static::assertSame(['id'], $propertyNames);
+ }
+
+ public function testCmsPageVersionReferenceFieldIsNotPrimaryKeyButRequired(): void
+ {
+ $field = $this->fields['cmsPageVersionId'];
+
+ static::assertInstanceOf(ReferenceVersionField::class, $field);
+ static::assertTrue($field->is(Required::class));
+ static::assertFalse($field->is(PrimaryKey::class));
+ }
+
+ public function testCmsPageAssociationIsManyToOne(): void
+ {
+ $association = $this->fields['cmsPage'];
+
+ static::assertInstanceOf(ManyToOneAssociationField::class, $association);
+ static::assertSame(CmsPageDefinition::class, $association->getReferenceClass());
+ }
+
+ /**
+ * The CmsPageExtension provides the reverse one-to-many side the DAL validator requires.
+ */
+ public function testCmsPageExtensionProvidesReverseAssociation(): void
+ {
+ $extensionFields = new FieldCollection();
+ (new CmsPageExtension())->extendFields($extensionFields);
+
+ $reverse = $this->fieldsByName($extensionFields)['blogEntries'] ?? null;
+
+ static::assertNotNull($reverse);
+ static::assertInstanceOf(OneToManyAssociationField::class, $reverse);
+ static::assertSame(BlogEntryDefinition::class, $reverse->getReferenceClass());
+ }
+
+ public function testTagsAssociationUsesTagMappingDefinition(): void
+ {
+ $association = $this->fields['tags'];
+
+ static::assertInstanceOf(ManyToManyAssociationField::class, $association);
+ // for many-to-many fields the reference class is the mapping definition
+ static::assertSame(BlogEntryTagMappingDefinition::class, $association->getReferenceClass());
+ static::assertSame(TagDefinition::class, $this->getToManyReferenceClass($association));
+ }
+
+ /**
+ * The TagExtension provides the reverse many-to-many side the DAL validator requires.
+ */
+ public function testTagExtensionProvidesReverseAssociation(): void
+ {
+ $extensionFields = new FieldCollection();
+ (new TagExtension())->extendFields($extensionFields);
+
+ $reverse = $this->fieldsByName($extensionFields)['blogEntries'] ?? null;
+
+ static::assertNotNull($reverse);
+ static::assertInstanceOf(ManyToManyAssociationField::class, $reverse);
+ static::assertSame(BlogEntryDefinition::class, $this->getToManyReferenceClass($reverse));
+ static::assertSame(BlogEntryTagMappingDefinition::class, $reverse->getReferenceClass());
+ static::assertTrue($reverse->is(CascadeDelete::class));
+ }
+
+ /**
+ * The teaser image lives in the translations; the media association on the entity is
+ * hydrated manually by the BlogSubscriber and must therefore stay out of the DAL
+ * field collection, but be documented as internal for the definition validator.
+ */
+ public function testMediaIsNotADalFieldButAnInternalHydratedAssociation(): void
+ {
+ static::assertArrayNotHasKey('media', $this->fields);
+ static::assertInstanceOf(TranslatedField::class, $this->fields['mediaId']);
+
+ $docComment = (new \ReflectionProperty(BlogEntryEntity::class, 'media'))->getDocComment();
+
+ static::assertIsString($docComment);
+ static::assertStringContainsString('@internal', $docComment);
+ }
+
+ public function testSetTagsDoesNotAcceptNull(): void
+ {
+ $parameter = (new \ReflectionMethod(BlogEntryEntity::class, 'setTags'))->getParameters()[0];
+
+ static::assertFalse($parameter->allowsNull());
+ }
+
+ /**
+ * @return array
+ */
+ private function defineFieldsByName(BlogEntryDefinition $definition): array
+ {
+ $method = new \ReflectionMethod(BlogEntryDefinition::class, 'defineFields');
+
+ return $this->fieldsByName($method->invoke($definition));
+ }
+
+ /**
+ * @return array
+ */
+ private function fieldsByName(FieldCollection $collection): array
+ {
+ $fields = [];
+
+ foreach ($collection as $field) {
+ $fields[$field->getPropertyName()] = $field;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Reads the (uncompiled) to-many reference class of a many-to-many association.
+ */
+ private function getToManyReferenceClass(object $association): string
+ {
+ $property = new \ReflectionProperty(ManyToManyAssociationField::class, 'toManyDefinitionClass');
+
+ return $property->getValue($association);
+ }
+}