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

Why cannot I use graph traversal? #533

Closed
bupy7 opened this issue Aug 23, 2017 · 12 comments
Closed

Why cannot I use graph traversal? #533

bupy7 opened this issue Aug 23, 2017 · 12 comments

Comments

@bupy7
Copy link
Contributor

bupy7 commented Aug 23, 2017

In doc http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#entity-object-graph-traversal said:

You can walk all the associations inside your entity models as deep as you want.

But I cannot. After request

$e = $container->get('Doctrine\ORM\EntityManager')->find('Telegram\Entity\State', 2);

$e->getParent() is equivalent NULL.

My entity map:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
    http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

  <entity name="Telegram\Entity\State" table="tgm_state" repository-class="Telegram\Repository\StateRepository">
    <indexes>
      <index name="tgm_state_idx_1" columns="t_user_id"/>
      <index name="tgm_state_idx_2" columns="type_id"/>
      <index name="tgm_state_idx_3" columns="parent_id"/>
    </indexes>

    <id name="id" type="integer" column="id">
      <generator strategy="IDENTITY"/>
    </id>

    <field name="typeId" type="smallint" column="type_id"/>
    <field name="name" type="string" column="name" length="255"/>
    <field name="value" type="params" column="value"/>
    <field name="createdAt" type="datetime" column="created_at"/>
    <field name="updatedAt" type="datetime" column="updated_at"/>

    <one-to-many field="children" target-entity="Telegram\Entity\State" mapped-by="parent"/>
    <many-to-one field="tUser" target-entity="User\Entity\Telegram">
      <join-columns>
        <join-column name="t_user_id" referenced-column-name="id"/>
      </join-columns>
    </many-to-one>
    <many-to-one field="parent" target-entity="Telegram\Entity\State" inversed-by="children">
      <join-columns>
        <join-column name="parent_id" referenced-column-name="id"/>
      </join-columns>
    </many-to-one>
  </entity>
</doctrine-mapping>

Entity:

namespace Telegram\Entity;

use Application\Entity\EntityAbstract;
use DateTime;
use User\Entity\Telegram;
use Zend\Stdlib\ParametersInterface;
use Zend\Stdlib\Parameters;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\PersistentCollection;

class State extends EntityAbstract
{
    /**
     * Life expiration a each state in minutes.
     */
    public const LIFE_EXPIRE = 15;
    // types
    public const TYPE_INPUT = 1;
    public const TYPE_FORM = 2;
    public const TYPE_PAGINATION = 3;

    /**
     * @var int
     */
    protected $id;
    /**
     * @var int
     */
    protected $typeId;
    /**
     * @var string
     */
    protected $name;
    /**
     * @var ParametersInterface
     */
    protected $value;
    /**
     * @var DateTime
     */
    protected $createdAt;
    /**
     * @var DateTime
     */
    protected $updatedAt;
    /**
     * @var Telegram
     */
    protected $tUser;
    /**
     * @var State|null
     */
    protected $parent;
    /**
     * @var ArrayCollection
     */
    protected $children;

    public function __construct()
    {
        $this->value = new Parameters;
        $this->createdAt = new DateTime;
        $this->updatedAt = new DateTime;
        $this->children = new ArrayCollection;
    }

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

    public function setTypeId(int $typeId): State
    {
        $this->typeId = $typeId;
        return $this;
    }

    public function getTypeId(): int
    {
        return $this->typeId;
    }

    public function setName(string $name): State
    {
        $this->name = $name;
        return $this;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setValue(ParametersInterface $value): State
    {
        $this->value = $value;
        return $this;
    }

    public function getValue(): ParametersInterface
    {
        return $this->value;
    }

    public function setCreatedAt(DateTime $createdAt): State
    {
        $this->createdAt = $createdAt;
        return $this;
    }

    public function getCreatedAt(): DateTime
    {
        return $this->createdAt;
    }

    public function setUpdatedAt(DateTime $updatedAt): State
    {
        $this->updatedAt = $updatedAt;
        return $this;
    }

    public function getUpdatedAt(): DateTime
    {
        return $this->updatedAt;
    }

    public function setTUser(Telegram $tUser): State
    {
        $this->tUser = $tUser;
        return $this;
    }

    public function getTUser(): Telegram
    {
        return $this->tUser;
    }

    public function setParent(?State $parent): State
    {
        $this->parent = $parent;
        return $this;
    }

    public function getParent(): ?State
    {
        return $this->parent;
    }

    public function getChildren(): PersistentCollection
    {
        return $this->children;
    }
}

Table data:

id t_user_id parent_id type_id name value created_at updated_at
1 1 NULL 1 newparty {} 2017-08-22 22:37:15 2017-08-22 22:37:15
2 2 1 2 newparty {"name": "New name", "cityId": null} 2017-08-22 22:38:49 2017-08-22 22:38:49

Packages info:

php: 7.1.8
doctrine/doctrine-orm-module: 1.1.4
@MatthiasKunnen
Copy link

I'm experiencing the same problem. Just out of sheer curiosity, what happens if you call
$e->getName() before calling $e->getParent()? Does getParent still return null?

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

@MatthiasKunnen Of course, because Telegram\Entity\State not have proxy wrapper after fetch.

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

@MatthiasKunnen I found out this and this. But I don't understand why in doc have ability to use graph traversal?

@MatthiasKunnen
Copy link

Then our problems are of a different kind. In my case, a proxy is in place, but using the getter of any association returns null until a common property (such as name) is fetched. Fetching a common property seems to initialize the entity and allows for associated entities to be fetched.

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

@MatthiasKunnen Please, write version your package here and php, and zend framework.

@MatthiasKunnen
Copy link

I tested on both version 0.9.2 and 1.1.4. I made a test case for the doctrine/doctrine2 repository, but everything seems to work there.

Test case:

<?php

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
 * @group MyTest
 */
class MyTest extends  \Doctrine\Tests\OrmFunctionalTestCase
{
    /**
     * {@inheritDoc}
     */
    protected function setUp()
    {
        parent::setUp();

        $this->_schemaTool->createSchema(
            [
                $this->_em->getClassMetadata(MyTestBuilding::class),
                $this->_em->getClassMetadata(MyTestCampus::class),
                $this->_em->getClassMetadata(MyTestTrivial::class),
            ]
        );
    }

    public function testIssue()
    {
        $trivial = new MyTestTrivial();
        $trivial->name = 'Trivial';
        $this->_em->persist($trivial);

        $building = new MyTestBuilding();
        $this->_em->persist($building);

        $campus = new MyTestCampus();
        $this->_em->persist($campus);

        $building->sequence = 1;

        $campus->buildings->add($building);
        $building->campus = $campus;
        $campus->trivial = $trivial;

        $this->_em->flush();
        $this->_em->clear();

        /** @var MyTestBuilding $building */
        $building = $this->_em->getRepository(MyTestBuilding::class)
            ->findOneBy([
                'sequence' => 1,
            ]);

        $this->assertNotNull($building->campus->trivial);
    }
}

/** @Entity */
class MyTestTrivial
{
    /**
     * @Id
     * @Column(type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    public $name;
}

/** @Entity */
class MyTestCampus
{
    /**
     * @Id
     * @Column(type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @OneToMany(
     *     targetEntity="\Doctrine\Tests\ORM\Functional\Ticket\MyTestBuilding",
     *     mappedBy="category"
     * )
     * @Annotation\Exclude()
     * @var Collection
     */
    public $buildings;

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    public $name;

    /**
     * @JoinColumn(name="trivial_id", referencedColumnName="id")
     * @OneToOne(
     *     targetEntity="\Doctrine\Tests\ORM\Functional\Ticket\MyTestTrivial",
     *     cascade={"persist", "remove"},
     *     orphanRemoval=true
     * )
     * @var MyTestTrivial
     */
    public $trivial;

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

/**
 * @Entity
 */
class MyTestBuilding
{
    /**
     * @ManyToOne(
     *     targetEntity="\Doctrine\Tests\ORM\Functional\Ticket\MyTestCampus",
     *     inversedBy="buildings"
     * )
     * @JoinColumn(name="campus_id", referencedColumnName="id",
     *     nullable=false, onDelete="CASCADE")
     * @var MyTestCampus
     */
    public $campus;

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

    /**
     * @Column(type="integer")
     * @var int
     */
    public $sequence;
}

This is a simplified version of the entities I'm using, but with the exact same relations.

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

@MatthiasKunnen You don't have to use public property in entity. Only private or protected for property and getters and setters for get/set value from entity. Probably, you have problems because of this.

@MatthiasKunnen
Copy link

Yeah, I'm aware of that. However, doctrine allows for public properties to be used instead of getters and setters. Would there be a way to test if this works when the DoctrineORMModule is used? Since this testcase passes in doctrine/doctrine2 but not when I'm using this module along with the "hounddog/doctrine-data-fixture-module": "^0.0.4".

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

@MatthiasKunnen I have run your test case in doctrine/orm:dev-2.5 package and that test have passed. The problem somewhere in doctrine/DoctrineORMModule.

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

My test case for doctrine/orm:dev-2.5 have passed too:

namespace Doctrine\Tests\ORM\Ticket;

use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
 * @group Bupy7TraversalTest
 */
class Bupy7TraversalTest extends  \Doctrine\Tests\OrmFunctionalTestCase
{
    /**
     * {@inheritDoc}
     */
    protected function setUp()
    {
        parent::setUp();

        $this->_schemaTool->createSchema(
            [
                $this->_em->getClassMetadata(State::class),
            ]
        );
    }

    public function testIssue()
    {
        $parent = new State();
        $parent->setTypeId(1)
            ->setName('newcommand1')
            ->setCreatedAt(new DateTime)
            ->setUpdatedAt(new DateTime);
        $this->_em->persist($parent);

        $child = new State();
        $child->setTypeId(2)
            ->setName('newcommand1')
            ->setValue(['name' => null])
            ->setCreatedAt(new DateTime)
            ->setUpdatedAt(new DateTime)
            ->setParent($parent);
        $this->_em->persist($child);

        $this->_em->flush();
        $this->_em->clear();

        /** @var State $child */
        $child = $this->_em->getRepository(State::class)->findOneBy([
                'typeId' => 2,
            ]);

        $this->assertNotNull($child->getParent());
    }
}

/**
 * @Entity
 */
class State
{
    /**
     * @var integer
     *
     * @Column(name="id", type="integer")
     * @Id
     * @GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var integer
     *
     * @Column(name="type_id", type="smallint")
     */
    private $typeId;

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

    /**
     * @var array
     *
     * @Column(name="value", type="json")
     */
    private $value = [];

    /**
     * @var\DateTime
     *
     * Column(name="created_at", type="datetime")
     */
    private $createdAt;

    /**
     * @var DateTime
     *
     * @Column(name="updated_at", type="datetime")
     */
    private $updatedAt;

    /**
     * @var Collection
     *
     * @OneToMany(targetEntity="State", mappedBy="parent")
     */
    private $children;

    /**
     * @var State
     *
     * @ManyToOne(targetEntity="State", inversedBy="children")
     * @JoinColumn(name="parent_id", referencedColumnName="id")
     */
    private $parent;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->children = new ArrayCollection();
    }

    /**
     * Set typeId
     *
     * @param integer $typeId
     *
     * @return State
     */
    public function setTypeId($typeId)
    {
        $this->typeId = $typeId;

        return $this;
    }

    /**
     * Get typeId
     *
     * @return integer
     */
    public function getTypeId()
    {
        return $this->typeId;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return State
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set value
     *
     * @param array $value
     *
     * @return State
     */
    public function setValue($value)
    {
        $this->value = $value;

        return $this;
    }

    /**
     * Get value
     *
     * @return array
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * Set createdAt
     *
     * @param DateTime $createdAt
     *
     * @return State
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * Get createdAt
     *
     * @return DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Set updatedAt
     *
     * @param DateTime $updatedAt
     *
     * @return State
     */
    public function setUpdatedAt($updatedAt)
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * Get updatedAt
     *
     * @return \DateTime
     */
    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Add child
     *
     * @param State $child
     *
     * @return State
     */
    public function addChild(State $child)
    {
        $this->children[] = $child;

        return $this;
    }

    /**
     * Remove child
     *
     * @param State $child
     */
    public function removeChild(State $child)
    {
        $this->children->removeElement($child);
    }

    /**
     * Get children
     *
     * @return Collection
     */
    public function getChildren()
    {
        return $this->children;
    }

    /**
     * Set parent
     *
     * @param State $parent
     *
     * @return State
     */
    public function setParent(State $parent = null)
    {
        $this->parent = $parent;

        return $this;
    }

    /**
     * Get parent
     *
     * @return State
     */
    public function getParent()
    {
        return $this->parent;
    }
}

Why don't work it in DoctrineORMModule?

@bupy7
Copy link
Contributor Author

bupy7 commented Aug 23, 2017

I found out what is the problem! I'm using EagerQuoteStrategy https://gist.github.com/Thinkscape/6713196 which quote field name (example, parent_id) and after it UnitOfWork cannot find a field using the clear name parent_id.

@MatthiasKunnen
Copy link

Congratulations on finding your bug 👍. For some mysterious reason, my problem disappeared after adding eager loading, clearing the data folder and orm cache, and then removing the eager loading again. Strange, but I'll take it.

@bupy7 bupy7 closed this as completed Aug 24, 2017
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

2 participants