phpstan-doctrine icon indicating copy to clipboard operation
phpstan-doctrine copied to clipboard

Cannot satisfy not nullable OneToOne inverse side

Open noemi-salaun opened this issue 5 years ago • 12 comments

I cannot find a way to satisfy the case

Property App\User::$stats type mapping mismatch: database can contain App\UserStats|null but property expects App\UserStats.

I have the following mapping :

<?php

class User
{

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * @var UserStats
     *
     * @ORM\OneToOne(targetEntity="App\UserStats", mappedBy="user")
     */
    protected $stats;

    // ... other properties

}

class UserStats
{
    
    /**
     * @var User
     *
     * @ORM\Id
     * @ORM\OneToOne(targetEntity="App\User", inversedBy="stats")
     */
    private $user;
    
    // ... other properties

}

If I add @ORM\JoinColumn(nullable=false) on User::$stats property then Doctrine is broken because JoinColumn annotation is expected on the owning side of the one to one relationship.

So I'm stuck with this case, because I can't explain to phpstan that my property isn't nullable :/

noemi-salaun avatar Dec 17 '19 14:12 noemi-salaun

I'm having the exact same problem. Did you find a workaround (other than just ignoring those errors)?

RichAtPowderBlue avatar Jan 30 '20 08:01 RichAtPowderBlue

No I didn't find a good solution. I gived up and add it to the ignored errors :/

noemi-salaun avatar Jan 30 '20 13:01 noemi-salaun

I will try to look into it.

lookyman avatar Jan 30 '20 14:01 lookyman

Thanks for replying @noemi-salaun I've ended up doing the same. Also thanks @lookyman - really appreciate it 😄

RichAtPowderBlue avatar Jan 30 '20 15:01 RichAtPowderBlue

I have a similar issue, except my use-case is the opposite - I have a non-nullable database field, but need the property itself to be nullable for creation (and then use a NotNull assert to prevent possible issues).

lukasluecke avatar Jun 18 '20 11:06 lukasluecke

@noemi-salaun I have the same, but with an additional @ORM\JoinColumn(nullable=false) on UserStats::$user. This tells Doctrine that there can be no UserStats with an empty user column. This aspect might already be included in @ORM\Id though ;-)

I think the real problem is: From looking at the mapping, you cannot answer the following question:

When a User is created, will there be a UserStats created in any case?

...cause the mapping does allow a User without a UserStats. In my app, I can answer with YES, cause in Users constructor I have:

$this->setStats(new UserStats($this));

... and cascade={"persist"}) on User::$stats.

@lookyman Checking for those two aspects might be an idea to resolve this.

Doctrine docs: https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/tutorials/composite-primary-keys.html#use-case-2-simple-derived-identity

ThomasLandauer avatar Jan 24 '21 22:01 ThomasLandauer

I have a similar problem

class Order
{
    /**
     * @var int
     *
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(name="id", type="integer", nullable=false)
     */
    private $id;

    /**
     * @var Billing
     *
     * @ORM\OneToOne(targetEntity="Billing", mappedBy="order", orphanRemoval=true, cascade={"persist"})
     */
    private $billing;

   // ...
}

class Billing
{
    /**
     * @var int
     *
     * @ORM\Id()
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var Order
     *
     * @ORM\OneToOne(targetEntity="Order", inversedBy="billing")
     * @ORM\JoinColumn(name="order_id", referencedColumnName="id", nullable=false)
     */
    private $order;

    /**
     * @var int
     *
     * @ORM\Column(name="order_id", type="integer", nullable=false)
     */
    private $orderId;

    // ....
}

PHPStan error message:

Property Order::$billing type mapping mismatch: database can contain Billing|null but property expects Billing

mitelg avatar Sep 14 '21 15:09 mitelg

@mitelg If there's no Billing for order ID 123 in the database, the $billing property for Order 123 will be null.

ondrejmirtes avatar Sep 14 '21 17:09 ondrejmirtes

@ondrejmirtes thanks for your reply

but an order without billing is not valid and should/could not happen 🤔 at least, I hope so 😅🙈

is there a way to ensure that?

I would have to look it up tomorrow, but on DB level, this should be prevented.

mitelg avatar Sep 14 '21 18:09 mitelg

I think it can't be enforced on the DB level.

ondrejmirtes avatar Sep 15 '21 06:09 ondrejmirtes

hey @ondrejmirtes

I had a deeper look in the code, and the order and the billing are created directly via SQL :see_no_evil: So due to erroneous behaviour at some point, it could indeed be possible that an order has no billing in the respective doctrine model :grimacing:

legacy software is fun... :tada:

thank you very much again!

mitelg avatar Sep 15 '21 07:09 mitelg