Search code examples
ormdoctrinesymfony4

Symfony 4 : Doctrine2 LAZY Fetch Collection still empty after calling the getter


I have an issue with some entities : according to the doctrine documentation, lazy loading should allow me to load my collection only when I call the getters, but it doesn't.

Here is the relation description :

One Location has many Venues, mapped by the Location "externalId" field.

Here is a simplified code sample of my entities and their relation :

// Location.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\LocationRepository")
 * @ORM\Table(name="Location")
 */
class Location extends Entity
{
    /**
     * @var integer
     * @ORM\Column(type="integer", unique=true, nullable=true)
     */
    protected $locationId;

    /**
     * @var integer
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer", nullable=false)
     */
    protected $externalId;

    /**
     * @var Venue[]|Collection
     * @ORM\OneToMany(targetEntity="App\Entity\Venue", mappedBy="locationExternalId", orphanRemoval=true, fetch="LAZY")
     */
    protected $venues;

    /**
     * Location constructor.
     * @param array $data
     */
    public function __construct(array $data)
    {
        $this->venues = new ArrayCollection;
        $this->hydrate($data);
    }

    /**
     * @return int
     */
    public function getLocationId()
    {
        return $this->locationId;
    }

    /**
     * @param int $locationId
     * @return $this
     */
    public function setLocationId($locationId)
    {
        $this->locationId = $locationId;
        return $this;
    }

    /**
     * @return Venue[]|Collection
     */
    public function getVenues()
    {
        return $this->venues;
    }

    /**
     * @param Venue[]|Collection $venues
     * @return $this
     */
    public function setVenues($venues)
    {
        $this->venues = $venues;
        return $this;
    }
}

// Venue.php
namespace App\Entity;

use App\Entity\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\VenueRepository")
 * @ORM\Table(name="Venue", indexes={@ORM\Index(name="IDX_Venue", columns={"location_id"})})
 */
class Venue extends Entity
{
    /**
     * @var integer
     * @ORM\Column(type="integer", unique=true, nullable=true)
     */
    protected $venueId;

    /**
     * @var integer
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer", nullable=false)
     */
    protected $externalId;

    /**
     * @var Location
     * @ORM\ManyToOne(targetEntity="App\Entity\Location", inversedBy="venues")
     * @ORM\JoinColumn(name="location_external_id", referencedColumnName="external_id", nullable=true)
     */
    protected $locationExternalId;

    /**
     * Venue constructor.
     * @param array $data
     */
    public function __construct(array $data)
    {
        $this->hydrate($data);
    }

    /**
     * @return int
     */
    public function getVenueId()
    {
        return $this->venueId;
    }

    /**
     * @param int $venueId
     * @return $this
     */
    public function setVenueId($venueId)
    {
        $this->venueId = $venueId;
        return $this;
    }

    /**
     * @return int
     */
    public function getExternalId()
    {
        return $this->externalId;
    }

    /**
     * @param int $externalId
     * @return $this
     */
    public function setExternalId($externalId)
    {
        $this->externalId = (int)$externalId;
        return $this;
    }

    /**
     * @return Location
     */
    public function getLocationExternalId()
    {
        return $this->locationExternalId;
    }

    /**
     * @param Location $locationExternalId
     * @return $this
     */
    public function setLocationExternalId($locationExternalId)
    {
        $this->locationExternalId = $locationExternalId;
        return $this;
    }
}

If I load a Location, Venues is not initialized, that's what is expected :

// Controller.php
use App\Entity\Basketball\Location;
use App\Entity\Basketball\Venue;

$location = $this->getDoctrine()->getRepository(Location::class)->findOneBy(['externalId' => 1]);
dump($location);

// Dump results
App\Entity\Location {#1438 ▼   #locationId: 1644  
#externalId: 1
#venues: Doctrine\ORM\PersistentCollection {#1450 ▼
    -snapshot: []
    -owner: App\Entity\Location {#1438}
    -association: array:15 [ …15]
    -em: Doctrine\ORM\EntityManager {#75 …11}
    -backRefFieldName: "locationExternalId"
    -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#1441 …}
    -isDirty: false
    #collection: Doctrine\Common\Collections\ArrayCollection {#1700 ▶}
    #initialized: false

But it should by calling the getter, and it's not working as expected

// Controller.php
dump($location->getVenues());

// Dump results
Doctrine\ORM\PersistentCollection {#1450 ▼
  -snapshot: []
  -owner: App\Entity\Location {#1438 ▶}
  -association: array:15 [ …15]
  -em: Doctrine\ORM\EntityManager {#75 …11}
  -backRefFieldName: "locationExternalId"
  -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#1441 …}
  -isDirty: false
  #collection: Doctrine\Common\Collections\ArrayCollection {#1700 ▼
    -elements: []
  }
  #initialized: false
}

Environment

  • Symfony v4.4.7
  • Doctrine v2.7 ("doctrine/doctrine-fixtures-bundle": "^3.3")
  • By specifying "fetch=EAGER", it works, but I don't want to...

Any idea? What should I do to initialize my collection?

Edit

Thanks to this Github issue I found a workaround by initializing the collection inside the getter but still; there should be a way to do it automatically :

public function getVenues()
{
    $this->venues->initialize();
    return $this->venues;
}

Solution

  • Does it really need to you? For initializing collection you need to call some method of this collection. If you are using JMSSerializer or Symfony Serializer you can just return the serialized data, serializers do all job for you, because they call toArray method, that initialize your collection.