Как правильно создать формы в Symfony для работы с ManyToMany отношениями?

Здравствуйте! Таблицы `post` и `language` взаимосвязаны через промежуточную таблицу `language_post`. 

<b>Сущность Post</b> 
<pre><code class="php">#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(options: ['unsigned' =&gt; true])]
    private ?int $id = null;

    #[ORM\Column(length: 150, unique: true)]
    private ?string $slug = null;

    /**
     * @var Collection&lt;int, LanguagePost&gt;
     */
    #[ORM\OneToMany(targetEntity: LanguagePost::class, mappedBy: 'post')]
    private Collection $languagePost;

    public function __construct()
    {
        $this-&gt;languagePost = new ArrayCollection();
    }
}</code></pre> 

<b>Сущность Language</b> 
<pre><code class="php">#[ORM\Entity(repositoryClass: LanguageRepository::class)]
class Language
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'tinyint', options: ['unsigned' =&gt; true, 'length' =&gt; 1])]
    private ?int $id = null;

    #[ORM\Column(length: 3)]
    private ?string $title = null;

    /**
     * @var Collection&lt;int, LanguagePost&gt;
     */
    #[ORM\OneToMany(targetEntity: LanguagePost::class, mappedBy: 'language')]
    private Collection $languagePost;

    public function __construct()
    {
        $this-&gt;languagePost = new ArrayCollection();
    }
}</code></pre> 

<b>Промежуточная сущность LanguagePost</b> 
<pre><code class="php">class LanguagePost
{
    #[ORM\Id]
    #[ORM\ManyToOne(targetEntity: Post::class, inversedBy: 'languagePost')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private ?Post $post = null;

    #[ORM\Id]
    #[ORM\ManyToOne(ta
</code></pre> 

Как вы можете описать структуру взаимосвязи между сущностями `Post`, `Language` и промежуточной сущностью `LanguagePost`?
  • 23 октября 2024 г. 13:33
Ответы на вопрос 2
Ваша структура для работы с ManyToMany отношениями в Symfony через промежуточную таблицу выглядит достаточно хорошо, но давайте немного уточним и завершим описание вашей промежуточной сущности `LanguagePost`, а также объясним, как связать всё это вместе.

### Объяснение структуры

1. **Сущность Post**:
   - Главная таблица, которая содержит информацию о постах.
   - Имеет связь `OneToMany` с промежуточной сущностью `LanguagePost`, что указывает на то, что один пост может быть связан с несколькими языками.

2. **Сущность Language**:
   - Главная таблица, которая содержит информацию о языках.
   - Также имеет связь `OneToMany` с промежуточной сущностью `LanguagePost`, что показывает, что один язык может быть связан с несколькими постами.

3. **Промежуточная сущность LanguagePost**:
   - Решает проблему хранения связей между `Post` и `Language`. Содержит два поля, которые мужчыменяют связи с Post и Language.
   - Используют составной первичный ключ из двух полей, `post` и `language`.

### Пример завершения промежуточной сущности LanguagePost

```php
#[ORM\Entity]
class LanguagePost
{
    #[ORM\Id]
    #[ORM\ManyToOne(targetEntity: Post::class, inversedBy: 'languagePost')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private ?Post $post = null;

    #[ORM\Id]
    #[ORM\ManyToOne(targetEntity: Language::class, inversedBy: 'languagePost')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private ?Language $language = null;

    // Геттеры и сеттеры
    public function getPost(): ?Post
    {
        return $this->post;
    }

    public function setPost(?Post $post): self
    {
        $this->post = $post;
        return $this;
    }

    public function getLanguage(): ?Language
    {
        return $this->language;
    }

    public function setLanguage(?Language $language): self
    {
        $this->language = $language;
        return $this;
    }
}
```

### Создание формы для работы с ManyToMany отношениями

Чтобы создать формализованную систему для создания/редактирования `Post` с использованием `Language`, вам потребуется создать форму для `Post`, которая будет содержать список доступных языков.

```php
use App\Entity\Post;
use App\Entity\Language;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('slug')
            ->add('languagePost', EntityType::class, [
                'class' => Language::class,
                'choice_label' => 'title',
                'multiple' => true, // Позволяет выбирать несколько
                'expanded' => true, // Если true, то будет чекбокс для каждого элемента
            ]);
    }
}
```

### Обработка данных в контроллере

В вашем контроллере вам нужно будет обрабатывать форму, сохранять `Post` и соответствующие языковые связи.

```php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

// ...
#[Route('/post/new', name: 'post_new')]
public function new(Request $request, EntityManagerInterface $entityManager)
{
    $post = new Post();
    $form = $this->createForm(PostType::class, $post);

    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        // Обработка языков
        $languages = $form->get('languagePost')->getData();
        foreach ($languages as $language) {
            $languagePost = new LanguagePost();
            $languagePost->setPost($post);
            $languagePost->setLanguage($language);
            $entityManager->persist($languagePost);
        }

        // Сохранение поста
        $entityManager->persist($post);
        $entityManager->flush();

        return $this->redirectToRoute('post_success');
    }

    return $this->render('post/new.html.twig', [
        'form' => $form->createView(),
    ]);
}
```

### Заключение

Теперь у вас есть полное представление
Вдруг кому-то еще понадобится. В итоге сделал с помощью трансформера. HiddenType ожидает текст, в качестве значения, но в language у нас объект Language. 
public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('language', HiddenType::class)
            ->get('language')
                ->addModelTransformer(new CallbackTransformer(
                    function ($language) {
                        return $language->getId();
                    },
                    function ($id): ?Language {
                        return $this->entityManager
                            ->getRepository(Language::class)
                            ->find($id);
                    }
                ));
    }


Поэтому когда подставляем значение в форму, с помощью трансформера передаем id. Когда форма отправляется на сервер, обратно переводим в объект Language. В идеале трансформер нужно перенести в отдельный класс, со всеми проверками и т.д, но тут просто в качестве примера.
Похожие вопросы