diff --git a/docs/dev/reference/widgets/picker.md b/docs/dev/reference/widgets/picker.md index 83df0c4a1..f012fd440 100644 --- a/docs/dev/reference/widgets/picker.md +++ b/docs/dev/reference/widgets/picker.md @@ -99,3 +99,83 @@ a serialized array. Since you do not know the length in advance, a blob column i This picker is used for content element and article include content element as well as the article teaser content element in order to pick and preview the referenced element or article. + + +## Managing Many-To-Many or One-To-Many relations with association tables + +With `'eval.multiple' => true`, Contao writes the picker value to the database as a serialized string. You might, however, want to maintain association tables, especially if you are using Doctrine Entities internally. + +To make the DCA picker compatible with association tables, you have to disable saving the picker and maintain the relations on your own via [load callback](/reference/dca/callbacks/#fields-field-load) and [save callback](/reference/dca/callbacks/#fields-field-load). + +First, disable saving the dca picker: + +```php +// ... +'myContentElements' => [ + 'inputType' => 'picker', + 'eval' => [ + 'doNotSaveEmpty' => true, + ], + // ... +], +// ... +``` + +Second, maintain the relations via callbacks (this example assumes Doctrine Entities internally): + + +```php +// src/EventListener/DataContainer/ContentElementsPickerListener.php +use App\Entity\Article; +use App\Entity\ContentElement; +use App\Repository\ArticleRepository; +use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback; +use Contao\DataContainer; +use Contao\StringUtil; +use Doctrine\ORM\EntityManagerInterface; + +class ContentElementsPickerListener +{ + public function __construct(private ArticleRepository $articleRepository, private EntityManagerInterface $em) + { + } + + #[AsCallback(table: 'tl_my_article', target: 'fields.elements.load')] + public function loadContentElements($value, DataContainer $dc = null): string + { + if (!$dc || !$dc->id) { + return ''; + } + + /** @var Article|null $article */ + $article = $this->articleRepository->find($dc->id); + + if (null === $article) { + return ''; + } + + return serialize(array_map(static fn (ContentElement $element) => $element->getId(), $article->getContentElements()->toArray())); + } + + #[AsCallback(table: 'tl_my_article', target: 'fields.categories.save')] + public function saveCategories($value, DataContainer $dc = null) + { + if (!$dc || !$dc->id) { + return null; + } + + /** @var Article|null $article */ + $article = $this->articleRepository->find($dc->id); + + if (null === $article) { + return null; + } + + $elements = StringUtil::deserialize($value, true); + + $article->setContentElements(array_map(fn (int $elmentId) => $this->em->getReference(ContentElement::class, $elementId), $elements)); + + return null; + } +} +```