jhard
jhard

Reputation: 127

Default value of Doctrine ORM association

I have the entity (such as below). I want to set some default values while creating. As you can see in __construct, it is easy to set the $name (string), but how can I set the $group? (for example I know that there is a group in database with id=122)

/**
 * @ORM\Entity
 */
class Person {
    private $id;

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

    /**
     * @ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
     * @ORM\JoinColumn(referencedColumnName="id")
     */
    private $group;

    public function setGroup(Group $group)
    {
        $this->group = $group;
        $group->addPerson($this);
    }

    // ... setters/getters

    //construct default Person
    public function __construct()
    {
        $this->setName("Mike");
        $this->setGroup($EXISTING_GROUP_FROM_MY_DB); // <<--------------
    }
}

Upvotes: 2

Views: 1827

Answers (2)

Will B.
Will B.

Reputation: 18416

The recommended method is to require the associated Entity object within the constructor arguments, optionally in combination with a Factory such as the Entity Repository, to supply the Group Entity during instantiation. This ensures the entity is always in a valid state.

src/Entity/Person.php

namespace App\Entity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\PersonRepository")
 */
class Person 
{
   //...

    public function __construct($name, Group $group)
    {
         $this->setName($name);
         $this->setGroup($group);
    }

    //...
}

src/Repsotory/PersonRepository.php

namespace App\Repsotory;

use App\Entity\Group;
use App\Entity\Person;

class PersonRepository
{
    const DEFAULT_GROUP = 122;

    public function create($name, Group $group = null)
    {
        if (null === $group) {
            $group = $this->_em->getReference(Group::class, self::DEFAULT_GROUP);
        }
        $person = new Person($name, $group);
        $this->_em->persist($person);
 
        return $person;
    }
}

This allows you to rely solely on the Doctrine ORM Entity Manager to maintain the default Group association.

$person = $em->getRepository(Person::class)->create('Mike');
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$em->flush();

This approach can be extended upon in Symfony to use Query services instead of the doctrine entity repository, to provide a central location that handles the instantiation of the entities.

In Symfony 3.4+ you can use Repository services to provide dependency injection for the repository, instead of using the EntityManagerInterface.

src/Service/PersonCreateQuery.php

namespace App\Service;

use App\Entity\Group;
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;

class PersonCreateQuery
{
    private $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function __invoke($name)
    {
        $group = $this->em->getReference(Group::class, 122);
        $person = new Person($name, $group);
        $this->em->persist($person);

        return $person;
    }
}

Now you can use dependency injection to retrieve the Query service and use it as desired, such as with a Symfony Form or Controller.

namespace App\Controller;

use App\Service\PersonCreateQuery;

class PersonController
{
    public function createAction(PersonCreateQuery $query)
    {
        $person = $query('Mike');
        $this->getDoctrine()->getManager()->flush();

        //...
    } 
}

Note: Usages of $em->getReference() can be replaced with $em->find(). Using $em->getReference() will prevent a query to the database but will throw an exception if the reference is invalid, while using $em->find() will return null instead.


Another approach is to use either Lifecycle Callbacks in the entity or an Event Listener to do more complex functionality. However, this will cause your entity to be instantiated in an invalid state until it is persisted.

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Person 
{
     const DEFAULT_GROUP = 122;

     /** @ORM\Column(type="string") */
     private $name = 'Mike';

    /**
     * @ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
     * @ORM\JoinColumn(referencedColumnName="id")
     */
    private $group;

    //....

    public function setGroup(Group $group)
    {
        $this->group = $group;
        $group->addPerson($this);
    }

    /**
     * @param LifecycleEventArgs $event
     * @ORM\PrePersist
     */
    public function onPrePersist(LifecycleEventArgs $event)
    {
       if (!$this->group instanceof Group) {
           /** set default group if not specified */
           $group = $event->getEntityManager()->getReference(Group::class,  self::DEFAULT_GROUP);
           $this->setGroup($group);
       }
    }

}

Now when you persist a Person entity it will add the group if it was not explicitly set elsewhere.

$person = new Person();
$person->setName('Foo Bar');
$em->persist($person); //persist or do nothing if already persisted

$group = $person->getGroup();
echo $group->getId(); //outputs: 122

$groupPerson = $group->getPerson();
echo $groupPerson->getName(); //outputs: Foo Bar
$em->flush(); //save to database

For sanity here are the links to the docs for the doctrine events:

Upvotes: 2

Thomas Kelley
Thomas Kelley

Reputation: 10292

I agree with moonwave99 that this is poor design. Here you are trying to access the database (through the Doctrine service) from a place that is not container-aware (i.e. it does not, and should not, know about Doctrine).

I had a similar issue recently... pretty much the same exact issue, actually. But I didn't want this logic to be inside the controller. So I wrote a service to take care of the User creation. And I gave that service access to the only other service it needed: Doctrine.

Here's an example, where a User is created with all available Roles:

namespace MyBundle\Entity;

class UserFactory
{
    private $doctrine;

    public function __construct($doctrine)
    {
        $this->doctrine = $doctrine;
    }

    public function generateNewUser($email, $password)
    {
        $user = new User();

        // Since you have access to the Doctrine service, you can use $this->doctrine
        // to do anything you would normally do in your controller with $this->getDoctrine()
        $roles = $this->doctrine->getEntityManager()->getRepository("MyBundle:Role")->findAll();

        foreach ($roles as $role)
        {
            $user->addRole($role);
        }

        return $user;
    }
}

Now register that service in config.yml or services.yml, remembering to pass the Doctrine service to it:

services:
    mybundle.factory.user:
        class: MyBundle\Entity\UserFactory
        arguments: ['@doctrine']

And that's it... Now, in your controller, you can create a new User by doing:

public function MyController()
{
    $user = $this->get("mybundle.factory.user")->generateNewUser("[email protected]", "password123");
}

Upvotes: 4

Related Questions