Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHP 7.4 Typed Ids musts be nullable to remove an entity #7999

Closed
leongersen opened this issue Jan 22, 2020 · 19 comments
Closed

PHP 7.4 Typed Ids musts be nullable to remove an entity #7999

leongersen opened this issue Jan 22, 2020 · 19 comments

Comments

@leongersen
Copy link

Bug Report

Q A
BC Break no
Version 2.7.0

Summary

This is closely related to #7940, which reports the UnitOfWork accessing properties before initialization. This was resolved in doctrine/persistence 1.3.6.

With that update, Ids must still be nullable to be able to remove the entity, as the UnitOfWork will be setting the id value back to null in \Doctrine\ORM\UnitOfWork::executeDeletions:

// Entity with this $oid after deletion treated as NEW, even if the $oid
// is obtained by a new entity because the old one went out of scope.
//$this->entityStates[$oid] = self::STATE_NEW;
if ( ! $class->isIdentifierNatural()) {
	$class->reflFields[$class->identifier[0]]->setValue($entity, null);
}

Adding a non-nullable type to a property and trying to remove it results in the following error:

Typed property App\Entity\MyEntity::$id must be int, null used

I'm not sure this necessarily qualifies as a bug in orm, but I'm not sure what else to call it. A potential solution would be to check if the reflField allows null (->getType()->allowsNull()).

@beberlei
Copy link
Member

Checking for allows null seems like the sensible approach.

@beberlei
Copy link
Member

Fix should be in doctrine/persistence TypedNoDefaultReflectionProperty class with a new setValue overwrite that handles the special case of setting to NULL by unitializing with unset().

@enumag
Copy link
Contributor

enumag commented Mar 24, 2020

@Alexandre-T
Copy link

@enumag I still have this bug, I use doctrine/reflection 1.2.1. The bug appears as soon as I added a ManyToMany relation between the same entity.

@enumag
Copy link
Contributor

enumag commented Mar 30, 2020

@Alexandre-T What is the version of your doctrine/persistence? If it is 1.3.6+, can you post the exact error message and relevant part of stack trace? It seems your case is elsewhere than what was fixed.

@Alexandre-T
Copy link

@enumag doctrine/persistence 1.3.7
I'm using a Symfony5 backend application with api-platform.
My standard User entity had a non-null Collection property stations which is initialized in the constructor. My backend was working. As soon as I had a ManyToOne relation to the same entity User, the serialization process failed because it accessed the initial property before initialization (in __sleep). I remove the @ before ORM to temporary remove the self ManyToOne relation, there is no more error.
Here is the error:

  1. DirectorCest: Edit user
    Test tests\api\Security\DirectorCest.php:editUser

[Error] Typed property Proxies_CG_\App\Entity\User::$ must not be accessed before initialization (in __sleep)

Scenario Steps:

  1. $I->sendPUT("/api/users/203",{"email":"foo@example.org","familyName":"foo","givenName":"foo","phone":"0000000042","plainPassword":"foo-bar-team","createdAt":"2018-01-20T13:58:21+00:00","updatedAt":"2018-01-20T13:58:21+00...}) at tests\api\Security\DirectorCest.php:153
  2. $I->haveHttpHeader("accept","application/json") at tests\api\Security\DirectorCest.php:151
  3. $I->haveHttpHeader("content-type","application/json") at tests\api\Security\DirectorCest.php:150
  4. $I->grabEntityFromRepository("App\Entity\User",{"email":"activist1@example.org"}) at tests\api\Security\DirectorCest.php:147

And here is the simplified code of my User entity:

//...
class User implements //....
{
  //...
  /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Station", inversedBy="users")
     *
     * @var Collection|Station[]
     *
     * @Groups({"owner:user:read", "animator:user:read"})
     */
  private Collection $stations;

    /**
     * Referent of this user. 
     *
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     */
    private ?User $referent = null;

   /**
     * User constructor.
     */
    public function __construct()
    {
        $this->stations = new ArrayCollection();
    }
}

@enumag
Copy link
Contributor

enumag commented Mar 31, 2020

@Alexandre-T Ah, actually that's a different issue. See doctrine/common#886.

@Ciloe
Copy link

Ciloe commented Apr 22, 2020

Hi, there are any news about this issue ? I have the same problem when I want to delete an entity....

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     **/
    public int $id;

The version of packages :
doctrine/reflection: 1.2.1
doctrine/persistence: 1.3.7

Typed property App\Entity\MyEntity::$id must be int, null used

in RuntimePublicReflectionProperty.php line 47
--
at ReflectionProperty->setValue(object(MyEntity), null)in RuntimePublicReflectionProperty.php line 47
at RuntimePublicReflectionProperty->setValue(object(MyEntity), null)in UnitOfWork.php line 1213
at UnitOfWork->executeDeletions(object(ClassMetadata))in UnitOfWork.php line 423

EDIT

It works in private property id. Because the PR https://github.com/doctrine/reflection/pull/35/files is only on private properties. Thanks to add this code on public properties ?

@oojacoboo
Copy link
Contributor

Honestly, after going through things with this for a while now, we've decided that allowing null for an auto_increment dbms generated value is the ideal functionality.

Firstly, it allows for us to tell if the entity has been persisted. Relying on the EntityManager is not sufficient. And, in a state where an object has been created and not persisted, that value being null, and accessible, is the most sane state/type IMO. If there was a more direct way to check if a value has been initialized, maybe that'd be better, but I'm not aware of a clean way of doing that right now (reflection isn't an option IMO).

@joaojacome
Copy link

I've added a fix for public properties here: doctrine/persistence#103

Just need a review :)

@elzozo13
Copy link

I also encountered this issue and read the previous posts.

I strongly disagree with @oojacoboo opinion saying the best solution is to make them nullable for the following reasons (I will refer to symfony 5 projects because letely that's what I've been working on, but I'm sure is rather general):

  1. When I do something in my code I check for all possible values, and treat all possible cases (it is a good practice and strongly suggest all devs to do this). I waited years for PHP to finally get strong types in order to be able to do that right and now I would love to be able to use them. If I see that an entity has a nullable property, somewhere in my code the following line will appear

if (is_null($entity->getProperty)) { // do something }

Making ids (or any other property for that matter but ids are affected mostly) nullable will pollute my code with thousands of useless checks making it less readable and less maintainable. Also a little bit slower but that is negligible in 99.99% of real life cases so that is not my main issue, but the useless code is.

  1. I'm using annotations to generate migrations (I know this practice is debatable but that's how we do it for a series of reasons I won't get in). Right now I have a column set as not nullable in annotations (and in db), and the entity property set as nullable. It is clear that is a discrepancy between the db and the code, and the whole point of the orm is to map that values in objects.

  2. I cannot use any code quality tools at their full capabilities (like https://github.com/rectorphp/rector or https://github.com/phpstan/phpstan) because they fail on all entities because annotations say is not nullable while the typehint sais it is in order to be able to delete.

  3. There are other minor reasons why this is bad but I think those 3 should suffice to make it clear that the current way it works needs to be improved. PHP 7+ gave us so many great improvements and would be a shame not to take advantage of them in code.

Fix suggestion:
Why not mimic the id property internally in em, and when deleting setting that as null, and check that? Is a simple fix imo. The only time when this issue appears is when a totally new entity is created, and the id hasn't been yet set (but that is not a problem because is never requested hence the exception is not thrown), and when an entity is deleted. A new method for related entities would also need to be added but again... should not be that complicated (at least from what I see in code). Would you be interested in a solution like this?

@oojacoboo
Copy link
Contributor

@elzozo13 I’d like to clarify that we almost always assume an auto generated “id” exists. If you’re frequently dealing with objects where that may not be the case, I’d agree and not advise you to make your id nullable

ruudk added a commit to ruudk/persistence that referenced this issue Mar 10, 2021
Related to doctrine/orm#7999

I'm using typed properties with a default value that is not null. Example:
```php
class Entity {
    /**
     * @Orm\Column(name="id", type="integer")
     * @Orm\Id
     * @Orm\GeneratedValue(strategy="AUTO")
     */
    private int $id = 0;
}

This works great. When inserting a new entity in the database, the auto incremented id is updated properly.

It means that entities that are not persisted will have a `0` as id and the rest will have another integer value.

But upon deleting an entity this becomes a problem. The ORM tries to set the `id` property value to `null` and that causes a TypeError:
```
TypeError: Typed property Entity::$id must be int, null used

vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1246
vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:441
vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:376
```

With this change applied, the default value of the `id` property `0` is used again. Solving the problem.
ruudk added a commit to ruudk/persistence that referenced this issue Mar 10, 2021
Related to doctrine/orm#7999

I'm using typed properties with a default value that is not null. Example:
```php
class Entity {
    /**
     * @Orm\Column(name="id", type="integer")
     * @Orm\Id
     * @Orm\GeneratedValue(strategy="AUTO")
     */
    private int $id = 0;

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

This works great. When inserting a new entity in the database, the auto incremented id is updated properly.

It means that entities that are not persisted will have a `0` as id and the rest will have another non-zero integer value.

But upon deleting an entity this becomes a problem. The ORM tries to set the `id` property value to `null` and that causes a TypeError:
```
TypeError: Typed property Entity::$id must be int, null used

vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1246
vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:441
vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:376
```

With this change applied, the default value of the `id` property `0` is used again.

Solving the problem.
ruudk added a commit to ruudk/persistence that referenced this issue Mar 10, 2021
Related to doctrine/orm#7999

I'm using typed properties with a default value that is not null. Example:
```php
class Entity {
    /**
     * @Orm\Column(name="id", type="integer")
     * @Orm\Id
     * @Orm\GeneratedValue(strategy="AUTO")
     */
    private int $id = 0;

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

This works great. When inserting a new entity in the database, the auto incremented id is updated properly.

It means that entities that are not persisted will have a `0` as id and the rest will have another non-zero integer value.

But upon deleting an entity this becomes a problem. The ORM tries to set the `id` property value to `null` and that causes a TypeError:
```
TypeError: Typed property Entity::$id must be int, null used

vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1246
vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:441
vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:376
```

With this change applied, the default value of the `id` property `0` is used again.

Solving the problem.
@ruudk
Copy link
Contributor

ruudk commented Mar 10, 2021

In my project I was using auto incremented integers and decided to type them like this:

class Entity {
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private int $id = 0;

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

It works great until I try to remove the entity. Giving me:

TypeError: Typed property Entity::$id must be int, null used

vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1246
vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:441
vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:376

I created a PR in doctrine/persistence to use the default property value when setting null.

I'm wondering: Does this make any sense? Is this crazy? Am I overlooking something, or is this the solution? 😊

@oojacoboo
Copy link
Contributor

@ruudk That's an undesirable hack. I'd rather have it be null than zero/int. I'm not really sure what you gain by doing this.

@thepercival
Copy link

I have experienced the same issue on php 8.
Cannot assign null to property MyObject::$id of type string|int

@guilliamxavier
Copy link
Contributor

IIUC there's a fix proposed: doctrine/persistence#103 but was hold as a BC break (wrong branch), then 2.0 was released without it 😞

@joaojacome
Copy link

I totally forgot that MR, and github wasn't sending me any updates on it - I'll try to fix the issues as soon as possible :)

@akenateb
Copy link

akenateb commented Apr 17, 2023

[FIXED] For those for whom these instructions do not work. The same issue here.

Fatal error: Uncaught TypeError: Typed property Application\Models\User::$id must be int, null used in C:\wamp64\www\phpdi\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php on line 1107

My composer.json:
{ "require": { "php-di/php-di": "^6.4", "nikic/fast-route": "^1.3", "twig/twig": "^3.5", "doctrine/orm": "*", "doctrine/cache": "^1.11", "symfony/cache": "*", "doctrine/annotations": "1.2.7" }, "autoload": { "psr-4": { "Application\\": "src/Application" } } }
Error given:

Fatal error: Uncaught TypeError: Typed property Application\Models\User::$id must be int, null used in C:\wamp64\www\phpdi\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php on line 1107

UnitOfWork->Line 1107:

if ( ! $class->isIdentifierNatural()) {
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
 }

Following what @leongersen said " A potential solution would be to check if the reflField allows null (->getType()->allowsNull())." He was right.

UnitOfWork->Line 1107:

if (!$class->isIdentifierNatural()) {
    $reflField = $class->reflFields[$class->identifier[0]];
    $reflType = $reflField->getType();

    if ($reflType !== null && $reflType->allowsNull()) {
        $reflField->setValue($entity, null);
    }
}

FIXED 4me at least. I can finally deleting.

@Warxcell
Copy link
Contributor

@ruudk That's an undesirable hack. I'd rather have it be null than zero/int. I'm not really sure what you gain by doing this.

avoiding null checks is one gain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests