Symfony DataMapper not updating object

Viewed 707

I'm using a DataMapper as per docs: https://symfony.com/doc/current/form/data_mappers.html to update a date from 1 of 3 fields in the form:

Form Class

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('period_start_daily', DateType::class, [
            'widget' => 'single_text',
            'label' => 'Day',
            'html5' => false,
            'attr' => ['class' => 'js-datepicker'],
            'mapped' => false,
            'required' => false
        ])
        ->add('period_start_weekly', DateType::class, [
            'widget' => 'single_text',
            'label' => 'Week',
            'html5' => false,
            'attr' => ['class' => 'js-datepicker'],
            'mapped' => false,
            'required' => false
        ])
        ->add('period_start_monthly', ChoiceType::class, [
            'required' => true,
            'label' => 'Month',
            'choice_loader' => new CallbackChoiceLoader(function () {
                $months = [];
                for ($i = 0; $i <= 12; $i++) {
                    $month = strtotime('first day of this month + ' . $i . ' months');
                    $months[date('M Y', $month)] = date('Y-m-d', $month);
                }
                return $months;
            }),
            'mapped' => false,
            'required' => false
        ])
...
$builder
        ->setDataMapper($this);
...
public function mapFormsToData($forms, &$data)
{
    /** @var FormInterface[] $forms */
    $forms = iterator_to_array($forms);

    switch (intval($forms['Type']->getViewData())){
        case 1:
            $start = $forms['period_start_daily']->getNormData();
            $end = clone $start;

            break;
        case 2:
            $start = $forms['period_start_weekly']->getNormData();
            $end = clone $start;
            $end->add(new \DateInterval('P6D'));
            break;
        case 3:
            $start = $forms['period_start_daily']->getNormData();
            $end = (new \DateTime(
                strtotime($this->getData()['period_start_monthly']->format('Y-m-d').' last day of month')
            ));
            break;
    }
    $data = (new DataSet())
        ->setPeriodStart($start)
        ->setPeriodEnd($end)
        ->setType($forms['Type']->getNormData())
        ->setUpload($forms['upload']->getNormData());

Controller

    $form = $this->createForm(DataSetType::class, $dataset);

    $form->handleRequest($request->getCurrentRequest());

    if ($form->isSubmitted() && $form->isValid()){
        $dataset
            ->setPeriodStart($form->getNormData()->getPeriodStart())
            ->setPeriodEnd($form->getNormData()->getPeriodEnd());
        $dataset->setType($form->getNormData()->getType());
        $dataset->setActive(false);
        $dataset->setUpload($form->getNormData()->getUpload());
        $dataset->getPeriodStart()->setTime(0, 0, 0);
        $dataset->getPeriodEnd()->setTime(0, 0, 0);

        $em->persist($dataset);
        $em->flush();

I expected the DataMapper to update my object directly, however I still have to copy over the data from $form->getNormData().

Symfony version is 4.2

1 Answers

I downloaded your project to debug it and I found a mistake - it was hard to detect :) I started thinking that there might be something wrong with Symfony 4 but not. Everything is fine and everything works fine.

Look:

enter image description here

spl_object_hash — Return hash id for given object

This function returns a unique identifier for the object. This id can be used as a hash key for storing objects, or for identifying an object, as long as the object is not destroyed. Once the object is destroyed, its hash may be reused for other objects.

$beforeHash and $afterHash have different values - therefore, it's not the same object

In DataSetType.php(line 138) you create new instance of DataSet losing a reference to previous object passed as the second parameter in mapFormsToData:

enter image description here

You should modify object in $data parameter instead creating new instance of DataSet.

Step 1:

Change this code:

$data = (new DataSet())
        ->setPeriodStart($start)
        ->setPeriodEnd($end)
        ->setType($forms['Type']->getNormData())
        ->setUpload($forms['upload']->getNormData())
        ->setActive($forms['active']->getNormData())
    ;

To this piece of code:

$data->setPeriodStart($start)
     ->setPeriodEnd($end)
     ->setType($forms['Type']->getNormData())
     ->setUpload($forms['upload']->getNormData())
     ->setActive($forms['active']->getNormData());

Step 2:

Remove unnecessary code from your controller. create method should look like this:

public function create(RequestStack $request)
{
    $dataset = new DataSet();

    $form = $this->createForm(DataSetType::class, $dataset);

    $form->handleRequest($request->getCurrentRequest());

    if ($form->isSubmitted() && $form->isValid()){
        $em = $this->getDoctrine()->getManager();
        $em->persist($dataset);
        $em->flush();

        if ($dataset->getUpload()) {
            $em->getRepository('App:DataSet')
                ->disableUploadOthers($dataset->getId(), $dataset->getType());
        }
        $this->addFlash('success', 'Dataset created.');
        return $this->redirectToRoute('dataset_index');
    }

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

Values in $dataset must change without your help :)


Documentation can be a bit confusing but note that the example from documentation is based on an immutable object

enter image description here

By the way - it would be good to extend the documentation and show how DataMapper works with Doctrine models...

Related