0

I have an object which is an instance of the entity class Workflow. This workflow has a property $states which is an instance of doctrines entity class ArrayCollection.

Part of my Workflow class:

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

class Workflow  {

    /**
     * @var integer
     */
    private $id;

    /**
     * @var Collection
     */
    private $states;

    /**
     * Workflow constructor.
     * @param ...t
     */
    public function __construct(...) {
        $this->states = new ArrayCollection();
        ...
    }

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

    public function addState(State $state) {
        $state->setWorkflow($this);
        $this->states->add($state);

        return $this;
    }

    ...

}

Workflows and Statess are mapped and get stored into the database. Here parts are the .orm.yml mapping files:

Workflow.orm.yml:

MyBundle\Entity\Workflow:
  type: entity
  id:
    id:
      type: integer
      generator: {strategy: AUTO}
  oneToMany:
    states:
      targetEntity: MyBundle\Entity\State
      mappedBy: workflow
      cascade: [persist, remove]
      orphanRemoval: true
  ...

State.orm.yml:

MyBundle\Entity\State:
  type: entity
  id:
    id:
      type: integer
      generator: {strategy: AUTO}
  manyToOne:
    workflow:
      targetEntity: MyBundle\Entity\Workflow
      inversedBy: states
      cascade: [persist]
  ...

In know that I have a Workflow named test stored with a State named release. I have a route with object parameters using Symfonys ParamConverter with type hinting.

Here is a part of the routing.yml:

my_route:
    path: /project/{project}/editWorkflow/{workflow}
    defaults: { _controller: "MyBundle:Test:createEditWorkflowFirstPart", workflow: 0 }

Now I call the route with an existing project and an existing workflow e.g. http://localhost/app_dev.php/de/testpra/project/79/editWorkflow/first/19 and expect Symfony to load the workflow in my action method.

My goal is to store the loaded workflow as a deep clone in my session and reload it in a second form part action method when the user submits an according button WorkflowStatesType::NEXT_FORM_PART.


Now the problem

When calling the createEditWorkflowFirstPartAction via route the method indeed holds a $workflow object which is an instance of Workflow but when I dump all states via dump($workflow->getStates() there are no elements in the ArrayCollection but when running the states in a loop foreach ($workflow->getStates() as $state) dump($state); Symfony dumps the State of the Workflow stored in the database.

I never had such a strange behaviour of Symfony so I really don´t know if its the dump that does not dump right or if the ArrayCollection just loads the states when it thinks they are needed.

When now calling unserialize(serialize($workflow)); to deep clone the $workflow the unserialized object does not dump any state when looping through states in a foreach.

Here is the createEditWorkflowFirstPartAction method:

public function createEditWorkflowFirstPartAction(Request $request, Project $project, Workflow $workflow = null) {

  $newWorkflow = false;

  if(!$workflow) {
    $workflow = new Workflow($project);
    $newWorkflow = true;
  }

  $workflowBeforeSubmit = unserialize(serialize($workflow));
  dump($workflow->getStates()); // Line 106 - Contains no elements
  dump($workflowBeforeSubmit->getStates()); // Line 107 - Contains no elements
  foreach ($workflow->getStates() as $state) dump($state); // Line 108 - Will print out my stored State
  foreach ($workflowBeforeSubmit->getStates() as $state) dump($state); // No states

  $firstFormPart = $this->createForm(WorkflowStatesType::class, $workflow);
  $firstFormPart->submit($request->get($firstFormPart->getName()), false);

  if($firstFormPart->isSubmitted() && $firstFormPart->isValid()) {
    ...
  }

  die();

  return $this->render('@MyBundle/Workflow/workflow_edit_create_first_part.html.twig', array(
    'form' => $firstFormPart->createView(),
  ));
}

And here is the according output (PraWorkflow = Workflow, PraTestController = TestController):

Output


  1. Why are the ArrayCollections empty and doesn´t hold the State (here id 26)?
  2. Why do I get the State when using a foreach loop? Does ArrayCollection access the database?
  3. Does unserialize(serialize(...)) work to deep clone an object with all child objects? If not, how should I do it without having any reference to one of the objects part of the $workflow object?
2
  • Why you don't used the JMS serializer bundle, it really helps to print your collections by just calling $this->get('jms_serializer')->toArray($workflow->getStates()), of course after adding the required annotation, check the documentation: jmsyst.com/libs/serializer/master/reference/annotations Commented Oct 10, 2017 at 23:15
  • 1
    I know it's late but this can really help other people debug there lazy loading in doctrine. As you sad it's not populated, thats because you have set lazy load as @Mocrates pointed out but if you want to dump it (var_dump or dump) you can do what i do use iterator_to_array() and dump that or do i foreach which is longer. example dump(iterator_to_array($workflow->getStates())) Commented Jan 19, 2019 at 17:58

2 Answers 2

2

Here's my implementation of deep cloning with collections :

/**
 * @ORM\Entity
 * @ORM\Table(name="clients")
*/
class Client
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    protected $id;

    // ...

    /**
     * @ORM\OneToMany(targetEntity="Address", mappedBy="client", cascade={"persist", "remove", "merge"}, orphanRemoval=true), fetch="EXTRA_LAZY")
     * @ORM\OrderBy("title"="ASC"})
     */
    protected $addresses;

    // ...

    public function __construct()
    {
        $this->addresses = new ArrayCollection();
    }

    // ...

    public function __clone()
    {
        if ($this->id)
        {
            $this->setId(null);
        }

        // cloning addresses
        $addressesClone = new ArrayCollection();
        foreach ($this->addresses as $address)
        {
            /* @var Address $address */
            $addressClone = clone $address;
            $addressClone->setClient($this);
            $addressesClone->add($addressClone);
        }
        $this->addresses = $addressesClone;
    }

    // ...

}

In controller, simply call $copy = clone $client; to get a perfect copy of your entity with collection.

Sign up to request clarification or add additional context in comments.

Comments

1

Every relation (whereas OnetoOne) are lazy loaded that's why doctrine don't populate your relation.

If you want it, Add an custom query in a repository and addSelect(alias.releationField) with a join part and your collection will be not empty.

You can also change a fetch mode in a entity by default to FETCH_EAGER

2 Comments

Adding fetch: EAGER to my states mapping in my Workflow.orm.ymlmapping file worked like a charm.
@goulashsoup It depends of your collection but if you don't need to load it every time (in my example, I don't want addresses for each find), fetch mode LAZY is your friend ...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.