Symfony 5 Embed Collection – форма прототипа не сохраняется

avatar
Pax
8 августа 2021 в 17:38
632
1
0

Я много часов пытаюсь сохранить свои встроенные коллекции в БД. Это не работает и даже делает что-то другое. У меня есть объект User OneToMany с адресом объекта. У меня есть форма для встраивания, но я не могу сохранить ее в БД. Хуже того, когда я добавляю свойство by_reference => false в поле для встраивания, оно удаляет существующий адрес, который у меня был :/ . Я не знаю, что я делаю неправильно. Я попытался добавить setAdress в свой объект User, но это не помогает. Я пытался добавить Cascade, но тоже не работает.
Вероятно, это проблема с моим контроллером (edit_profil), но я не вижу, с каким именно.

Теперь я получаю сообщение об ошибке Не удалось определить тип доступа для свойства "адрес" в классе "App\Entity\User".

ОБНОВЛЕНИЕ1 с параметром by_reference, установленным на false, теперь у меня есть эта ошибка: Ожидаемый аргумент типа «App\Entity\Address or null», «экземпляр Doctrine\ORM\PersistentCollection», указанный в пути к свойству «адрес». ОБНОВЛЕНИЕ2 Я удалил сеттер, потому что у меня есть метод добавления и удаления. Я также использовал множественное и единственное число для адреса, как это делается с тегом в документации. Теперь я вижу, что адрес формы не отправлен (он даже удаляет существующий) enter image description here (удаляю в коде то, что не нужно) Вот мои файлы:

RegistrationFormType

use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\CallbackTransformer;


class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('roles',ChoiceType::class,[
                'choices'=>[
                'Specialist'=>'Role_Specialist',
                'Parent'=>'Role_Parent',
                'Center'=>'Role_Center',],
                'label'=>"Je m'inscris en tant que"])
            ->add('name')
      //...//
            ->add('addresses',CollectionType::class,[
                'entry_type' => AddressType::class,
                'entry_options' => ['label' => false],
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' =>false,


            ])
        ;

Объект пользователя:

<?php

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;


/**
 * @ORM\Entity(repositoryClass=UserRepository::class)
 * @UniqueEntity(fields={"email"}, message="There is already an account with this email")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

//...//

    /**
     * @ORM\OneToMany(targetEntity=Address::class, mappedBy="user", orphanRemoval=true, cascade={"persist"})
     */
    private $addresses;


    public function __construct()
    {
        $this->category = new ArrayCollection();
        $this->schedules = new ArrayCollection();
        $this->bookings = new ArrayCollection();
        $this->addresses = new ArrayCollection();
    }
//...//

/**
 * @return Collection|Addresses[]
 */
public function getAddresses(): Collection
{
    return $this->addresses;
}


public function addAddress(Address $address): self
{
    $address->setUser($this);

    $this->adresse->add($address);
//    if (!$this->addresses->contains($address)) {
//        $this->addresses[] = $address;
//        $address->setUser($this);
//    }
    return $this;
}

public function removeAddress(Address $address): self
{
    if ($this->addresses->removeElement($address)) {
        // set the owning side to null (unless already changed)
        if ($address->getUser() === $this) {
            $address->setUser(null);
        }
    }

    return $this;
}

}

Контроллер регистрации

<?php

namespace App\Controller;

use App\Entity\Address;
use App\Entity\User;
use App\Form\AddressType;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use App\Security\LoginFormAuthenticator;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;

class RegistrationController extends AbstractController
{
    private $emailVerifier;

    public function __construct(EmailVerifier $emailVerifier)
    {
        $this->emailVerifier = $emailVerifier;
    }

//...//
    /**
     * @Route("/profil/{id}", name="edit_account")
     */
    public function editUser(Request $request,int $id)
    {
        $entityManager = $this->getDoctrine()->getManager();
        //Récupération de l'entité User avec l'ID passé et création du formulaire
        $repoUser = $this->getDoctrine()
        ->getRepository(User::class);
        $user = $repoUser->findOneBy([
            'id'=>$id,
        ]);
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);


        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($user);
            $entityManager->flush();
            $this->addFlash('success', 'User Created! Knowledge is power!');
            return $this->redirectToRoute('edit_account',[
                'id'=>$id,
            ]);
        }
        return $this->render('profil/edit_profil.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }
    /**
     * @Route("/verify/email", name="app_verify_email")
     */
    public function verifyUserEmail(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

        // validate email confirmation link, sets User::isVerified=true and persists
        try {
            $this->emailVerifier->handleEmailConfirmation($request, $this->getUser());
        } catch (VerifyEmailExceptionInterface $exception) {
            $this->addFlash('verify_email_error', $exception->getReason());

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

        // @TODO Change the redirect on success and handle or remove the flash message in your templates
        $this->addFlash('success', 'Your email address has been verified.');

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

}

Адрес объекта

<?php

namespace App\Entity;

use App\Repository\AddressRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=AddressRepository::class)
 */
class Address
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $type;

    /**
     * @ORM\Column(type="integer")
     */
    private $housenumber;

    /**
     * @ORM\Column(type="string", length=10, nullable=true)
     */
    private $additional_info;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $street;

    /**
     * @ORM\OneToMany(targetEntity=Schedule::class, mappedBy="address_fk")
     */
    private $schedules;

    /**
     * @ORM\Column(type="float", nullable=true)
     */
    private $latitude;

    /**
     * @ORM\Column(type="float", nullable=true)
     */
    private $longitude;


    /**
     * @ORM\Column(type="string", length=255)
     */
    private $city;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $region;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $country;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $postalcode;

    /**
     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="addresses")
     * @ORM\JoinColumn(nullable=false)
     */
    private $user;

    public function __construct()
    {
        $this->schedules = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getType(): ?string
    {
        return $this->type;
    }

    public function setType(string $type): self
    {
        $this->type = $type;

        return $this;
    }

    public function getHousenumber(): ?int
    {
        return $this->housenumber;
    }

    public function setHousenumber(int $housenumber): self
    {
        $this->housenumber = $housenumber;

        return $this;
    }

    public function getAdditionalInfo(): ?string
    {
        return $this->additional_info;
    }

    public function setAdditionalInfo(?string $additional_info): self
    {
        $this->additional_info = $additional_info;

        return $this;
    }

    public function getStreet(): ?string
    {
        return $this->street;
    }

    public function setStreet(string $street): self
    {
        $this->street = $street;

        return $this;
    }

    /**
     * @return Collection|Schedule[]
     */
    public function getSchedules(): Collection
    {
        return $this->schedules;
    }

    public function addSchedule(Schedule $schedule): self
    {
        if (!$this->schedules->contains($schedule)) {
            $this->schedules[] = $schedule;
            $schedule->setAddressFk($this);
        }

        return $this;
    }

    public function removeSchedule(Schedule $schedule): self
    {
        if ($this->schedules->removeElement($schedule)) {
            // set the owning side to null (unless already changed)
            if ($schedule->getAddressFk() === $this) {
                $schedule->setAddressFk(null);
            }
        }

        return $this;
    }

    public function getLatitude(): ?float
    {
        return $this->latitude;
    }

    public function setLatitude(?float $latitude): self
    {
        $this->latitude = $latitude;

        return $this;
    }

    public function getLongitude(): ?float
    {
        return $this->longitude;
    }

    public function setLongitude(?float $longitude): self
    {
        $this->longitude = $longitude;

        return $this;
    }

    public function getCity(): ?string
    {
        return $this->city;
    }

    public function setCity(string $city): self
    {
        $this->city = $city;

        return $this;
    }

    public function getRegion(): ?string
    {
        return $this->region;
    }

    public function setRegion(?string $region): self
    {
        $this->region = $region;

        return $this;
    }

    public function getCountry(): ?string
    {
        return $this->country;
    }

    public function setCountry(string $country): self
    {
        $this->country = $country;

        return $this;
    }

    public function getPostalcode(): ?string
    {
        return $this->postalcode;
    }

    public function setPostalcode(?string $postalcode): self
    {
        $this->postalcode = $postalcode;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): self
    {
        $this->user = $user;

        return $this;
    }
}

Edit_profil Twig

{% extends 'base.html.twig' %}

{#{% macro niceForm(address) %}#}
{#    <div class="form-group col-md-4">{{ form_row(address.type) }}</div>#}
{#    <div class="form-row">#}
{#        <div class="form-group col-md-4">{{ form_row(address.street) }}</div>#}
{#        <div class="form-group col-md-4">{{ form_row(address.housenumber) }}</div>#}
{#        <div class="form-group col-md-4">{{ form_row(address.additional_info) }}</div>#}
{#    </div>#}
{#    <div class="form-row">#}
{#        <div class="form-group col-md-3">{{ form_row(address.postalcode) }}</div>#}
{#        <div class="form-group col-md-3">{{ form_row(address.city) }}</div>#}
{#        <div class="form-group col-md-3">{{ form_row(address.region) }}</div>#}
{#        <div class="form-group col-md-3">{{ form_row(address.country) }}</div>#}
{#    </div>#}
{#{% endmacro %}#}
{#{% import _self as formMacros %}#}
{% block title %}Edit Profil{% endblock %}
{% block body %}

    {% for flashError in app.flashes('verify_email_error') %}
        <div class="alert alert-danger" role="alert">{{ flashError }}</div>
    {% endfor %}
    <div class="container">
    {{ form_start(registrationForm) }}
    <div class="form-group">{{ form_row(registrationForm.roles) }}</div>
    <div class="form-row">
        <div class="form-group col-md-6">{{ form_row(registrationForm.name) }}</div>
        <div class="form-group col-md-6">{{ form_row(registrationForm.firstname) }}</div>
    </div>
    <div class="form-row">
        <div class="form-group col-md-6">{{ form_row(registrationForm.email) }}</div>
        <div class="form-group col-md-6">{{ form_row(registrationForm.plainPassword, {
                label: 'Password'
            }) }}</div>
    </div>
    <div class="form-row">
        <div class="form-group col-md-2">{{ form_row(registrationForm.prefix) }}</div>
        <div class="form-group col-md-3">{{ form_row(registrationForm.phone) }}</div>
        <div class="form-group col-md-3">{{ form_row(registrationForm.NISS, {label: 'NISS'}) }}</div>
        <div class="form-group col-md-4">{{ form_row(registrationForm.BCE, {label: 'BCE'}) }}</div>
    </div>
    </div>
{#        @TODO formatter pour qu'on differencie bien les addresses entre elles par block. Cf block en dessous#}
    <div class="container">

        <h3>Addresses</h3>


        <ul class="addresses" data-index="{{ registrationForm.addresses|length > 0 ? registrationForm.addresses|last.vars.name + 1 : 0 }}" data-prototype="{{ form_widget(registrationForm.addresses.vars.prototype)|e('html_attr') }}">
            {% for address in registrationForm.addresses %}
            <li>{{ form_row(address.type) }}</li>
                <li>{{ form_row(address.street) }}</li>
                <li>{{ form_row(address.housenumber) }}</li>
                <li>{{ form_row(address.additional_info) }}</li>

                <li>{{ form_row(address.postalcode) }}</li>
                <li>{{ form_row(address.city) }}</li>
                <li>{{ form_row(address.region) }}</li>
                <li>{{ form_row(address.country) }}</li>
            {% endfor %}
        </ul>

        <button type="button" class="add_item_link" data-collection-holder-class="addresses">Add an address</button>

        <button type="submit" id="submitRegister" class="btn">Register</button>

        {{ form_end(registrationForm) }}

    </div>
    <script>
        const addFormToCollection = (e) => {
            const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass);

            const item = document.createElement('li');

            item.innerHTML = collectionHolder
                .dataset
                .prototype
                .replace(
                    /__name__/g,
                    collectionHolder.dataset.index
                );

            collectionHolder.appendChild(item);

            collectionHolder.dataset.index++;
        };
        document
            .querySelectorAll('.add_item_link')
            .forEach(btn => btn.addEventListener("click", addFormToCollection));

    </script>

Надеюсь, один из вас сможет увидеть, какая у меня проблема :)

Спасибо

Источник
Will B.
8 августа 2021 в 17:56
0

Вам не нужно создавать экземпляр формы AddressType, поскольку она встроена в форму RegistrationType и отображается в вашем шаблоне из form_row(address.type). Удалите $formAddress = $this->createForm(AddressType::class, $address); и все ссылки на него. Затем добавьте адрес в User в вашем контроллере $user->addAddress($address). Также ваша ссылка setAdress(?Adress $adress): должна использовать коллекцию $this->address = new ArrayCollection($address ?? []), хотя я рекомендую удалить этот метод.

Pax
8 августа 2021 в 18:57
0

Спасибо за ответ. Я заметил что-то непонятное в моем вопросе. Функция, отображающая мою ветку edit_profil, называется editUser. Первая функция используется для регистрации пользователя в первый раз, и именно здесь я создаю экземпляр AdresseType. Идея заключалась в том, чтобы иметь одну форму с одним адресом для заполнения. На самом деле мой вопрос был больше для editUser. Извините за путаницу и спасибо за быстрый ответ!

Ответы (1)

avatar
Pax
10 августа 2021 в 13:41
0

Проблема решена.

Спасибо за помощь, Уилл Б. Мне удалось решить мою проблему, которая исходила от моего контроллера. Я использовал то, что было объяснено в документе (11-е чтение было правильным). В https://symfony.com/doc/current/form/form_collections.html пункт доктрины постоянства был очень полезен (и это не только для удаления, хороший урок), мне просто нужно было добавить setUser для моих адресов в цикле. Моя вина и еще раз спасибо! Вот мой контроллер:

/**
     * @Route("/profil/{id}", name="edit_account")
     */
    public function editUser(Request $request,int $id)
    {
        $entityManager = $this->getDoctrine()->getManager();
        //Récupération de l'entité User avec l'ID passé et création du formulaire
        $repoUser = $this->getDoctrine()
        ->getRepository(User::class);
        $user = $repoUser->findOneBy([
            'id'=>$id,
        ]);
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);
        $originalAddresses = new ArrayCollection();

        // Create an ArrayCollection of the current Tag objects in the database
        foreach ($user->getAddresses() as $address) {
            $address->setUser($user);
            $originalAddresses->add($address);
        }
        if ($form->isSubmitted() && $form->isValid()) {
            foreach ($originalAddresses as $address) {
                if (false === $user->getAddresses()->contains($address)) {
                    // remove the Task from the Tag
                    $address->getUser()->removeElement($user);

                    // if it was a many-to-one relationship, remove the relationship like this
                    // $tag->setTask(null);

                    $entityManager->persist($address);
                }
            }
                }
        $entityManager->persist($user);
        $entityManager->flush();
        return $this->render('profil/edit_profil.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }

Еще раз спасибо!