-1

I use DTOs as the data_class for Symfony form types. There is one thing that does not work for me when I use typed properties (PHP 7.4) in these DTOs.

EXAMPLE:

class ProductDto
{
    /*
     * @Assert\NotBlank
     */
    public string $title;
}

This generally seems to work quite well – in case the user submits the form with a blank title or description, the validation kicks in and the form is displayed with validation warnings.

BUT THERE IS A PROBLEM when data is added while creating a form (e.g. the edit form):

$productDto = new ProductDto();
$productDto->title = 'Foo';
$form = $this->createForm(ProductFormType::class, $productDto);

Initially the form is displayed as expected with Foo as the value for the title. When a user clears the title input form field and submits the form an exception like this is thrown:

Typed property `App\Form\Dto\ProductDto::$title` must be string, null used

As far as I can see this is caused by the fact that during Form->handleRequest() the title is set to null after it was set to "Foo" before, right?

Is there a solution for this problem?

Mark Watney
  • 123
  • 1
  • 16
  • One way of doing this is changing `public string` to `public ?string` to make it nullable, another is adding default value of $title to be empty string `public string $title = '' ` and another is to add constructor and set the property value there. – Slavian Feb 05 '21 at 15:27
  • @Slavian Thx. I think `?` is not optimal, right? An empty string would indeed be an option. What would you do with e.g. `public Foo $foo;`? – Mark Watney Feb 05 '21 at 16:11
  • it really depends on the context, if I have a property which is a class as it is the example you have given, I would think if should the property be null or should it be always the class no matter what. It really depends on what you want to achieve. – Slavian Feb 05 '21 at 18:46
  • `'empty_data' => ''` on the form field might work – Jakumi Feb 05 '21 at 21:51
  • @Jakumi `'empty_data' => ''` seems to work in combination with `@Assert\NotBlank` and string properties. But it is not an option for e.g. `public Foo $foo;`. – Mark Watney Feb 07 '21 at 17:57
  • yes, the solution for the original problem doesn't necessarily work for a different problem. I would just make all fields in the dto nullable, which is waaaay easier and also clean, considering the form component will set null if a value is missing. assert\notnull should solve this then. *unsetting* a property like in your answer below I would really, really avoid like the pest. – Jakumi Feb 07 '21 at 22:15

2 Answers2

0

Since PHP 7.4 introduces type-hinting for properties, it is particularly important to provide valid values for all properties, so that all properties have values that match their declared types.

A property that has never been assigned doesn't have a null value, but it is on an undefined state, which will never match any declared type. undefined !== null.

Here is an example:

<?php

class Foo
{
    private int $id;
    private ?string $val;
    
    public function __construct(int $id) 
    {
        $this->id = $id;
    }
}

For the code above, if you did:

<?php

  foo = new Foo(1);
  $foo->getVal();

You would get:

Fatal error: Uncaught Error: Typed property Foo::$val must not be accessed before initialization

See this post for more details https://stackoverflow.com/a/59265626/3794075 and see this bug https://bugs.php.net/bug.php?id=79620

Houssem ZITOUN
  • 644
  • 1
  • 8
  • 23
  • Thanks, but the "must not be accessed before initialization" is not the problem I currently have. – Mark Watney Feb 07 '21 at 17:59
  • I think it is the some philosophy, behind the scene probably `$this->createForm(ProductFormType::class, $productDto);` will affect null to `$title` – Houssem ZITOUN Feb 07 '21 at 20:54
-1

This is what I just came up with:

DTO:

use GenericSetterTrait;

/**
 * @Assert\NotBlank
 */
public string $title;

public function setTitle(?string $title): void
{
    $this->set('title', $title);
}

/**
 * @Assert\NotNull
 */
public Foo $foo;

public function setFoo(?Foo $foo): void
{
    $this->set('foo', $foo);
}

Trait:

trait GenericSetterTrait
{
    private function set(string $propertyName, $value): void
    {
        if ($value === null) {
            unset($this->{$propertyName});
        } else {
            $this->{$propertyName} = $value;
        }
    }
}

Seems to work. What do you think? Any objections?

Mark Watney
  • 123
  • 1
  • 16