0

I'm currently working on a project for which i have to deal with a lot of users fields (+/- 80). My first approach was (and still is) to vertically partition the user table in 4 tables : main informations, administrative informations, banking informations, statistics informations. The ids of the 3 extra tables rows refer to the auto incremented id of the main table.

My questions are :

  1. Is it relevant to do so ?
  2. It seems like, with the way i did the association, Doctrine isn't able to flush one object into database in one shot :

Entity of type MyVendor\User\UserBundle\Entity\UsersInfosBank is missing an assigned ID for field 'id_user'. The identifier generation strategy for this entity requires the ID field to be populated before EntityManager#persist() is called. If you want automatically generated identifiers instead you need to adjust the metadata mapping accordingly.

The project will be pushed into production in 3 weeks, so we're still able to go back to a more traditionnal way to store these datas into one huge table…

****************UPDATE*****************

WHOLE CODE LOGIC BELOW*****************


I just removed most of the properties and methods which are not concerned by my problem…

And yes, i'm using FOSUserBundle ;)

UsersMain

<?php

namespace LCP\User\UserBundle\Entity;

use FOS\UserBundle\Model\GroupableInterface;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * UsersMain
 *
 * @ORM\Table(name="lcp_users_main")
 * @ORM\Entity
 */
class UsersMain extends BaseUser 
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;


    /**
     *
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersInfosBank", mappedBy="id_user", cascade={"persist"})
     */
    private $infosBank;

    /**
     *
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersApodis", mappedBy="id_user", cascade={"persist"})
     */
    private $apodis;

    /**
     *
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersInfosAdmin", mappedBy="id_user", cascade={"persist"})
     */
    private $infosAdmin;

    /**
     *
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersInfosStats", mappedBy="id_user", cascade={"persist"})
     */
    private $infosStats;



    /**
     * Constructor
     */
    public function __construct()
    {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
        $this->creationDate = new \Datetime();
    }

    /**
     * Set apodis
     *
     * @param \LCP\User\UserBundle\Entity\UsersApodis $apodis
     * @return UsersMain
     */
    public function setApodis(\LCP\User\UserBundle\Entity\UsersApodis $apodis = null)
    {
        if(is_null($this->apodis) && is_null($apodis)){
            $this->apodis = new \LCP\User\UserBundle\Entity\UsersApodis();
        } else {
            $this->apodis = $infosBank;
        }

        $this->apodis->setIdUser($this);

        return $this;
    }

    /**
     * Get apodis
     *
     * @return \LCP\User\UserBundle\Entity\UsersApodis 
     */
    public function getApodis()
    {
        if(is_null($this->infosAdmin)){
            $this->infosAdmin = new \LCP\User\UserBundle\Entity\UsersApodis();
        }
        return $this->apodis;
    }

    /**
     * Set infosAdmin
     *
     * @param \LCP\User\UserBundle\Entity\UsersInfosAdmin $infosAdmin
     * @return UsersMain
     */
    public function setInfosAdmin(\LCP\User\UserBundle\Entity\UsersInfosAdmin $infosAdmin = null)
    {
        if(is_null($this->infosAdmin) && is_null($infosAdmin)){
            $this->infosAdmin = new \LCP\User\UserBundle\Entity\UsersInfosAdmin();
        } else {
            $this->infosAdmin = $infosAdmin;
        }

        $this->infosAdmin->setIdUser($this);

        return $this;
    }

    /**
     * Get infosAdmin
     *
     * @return \LCP\User\UserBundle\Entity\UsersInfosAdmin 
     */
    public function getInfosAdmin()
    {
        if(is_null($this->infosAdmin)){
            $this->infosAdmin = new \LCP\User\UserBundle\Entity\UsersInfosAdmin();
        }
        return $this->infosAdmin;
    }

    /**
     * Set infosStats
     *
     * @param \LCP\User\UserBundle\Entity\UsersInfosStats $infosStats
     * @return UsersMain
     */
    public function setInfosStats(\LCP\User\UserBundle\Entity\UsersInfosStats $infosStats = null)
    {
        if(is_null($this->infosStats) && is_null($infosStats)){
            $this->infosStats = new \LCP\User\UserBundle\Entity\UsersInfosStats();
        } else {
            $this->infosStats = $infosAdmin;
        }

        $this->infosStats->setIdUser($this);

        return $this;
    }

    /**
     * Get infosStats
     *
     * @return \LCP\User\UserBundle\Entity\UsersInfosStats 
     */
    public function getInfosStats()
    {
        if(is_null($this->infosStats)){
            $this->infosStats = new \LCP\User\UserBundle\Entity\UsersInfosStats();
        }
        return $this->infosStats;
    }



}

UsersInfosAdmin

<?php

namespace LCP\User\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * UsersInfosAdmin
 *
 * @ORM\Table(name="lcp_users_infos_admin", indexes={@ORM\Index(name="pharmacyCip_idx", columns={"pharmacy_cip"})})
 * @ORM\Entity
 */
class UsersInfosAdmin
{

    /**
     * @var integer
     * @ORM\Id
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersMain", cascade={"persist"}, inversedBy="infosAdmin")
     */
    private $id_user;


    /**
     * Set id_user
     *
     * @param \LCP\User\UserBundle\Entity\UsersMain $idUser
     * @return UsersInfosAdmin
     */
    public function setIdUser(\LCP\User\UserBundle\Entity\UsersMain $idUser)
    {
        $this->id_user = $idUser;

        return $this;
    }

    /**
     * Get id_user
     *
     * @return \LCP\User\UserBundle\Entity\UsersMain 
     */
    public function getIdUser()
    {
        return $this->id_user;
    }
}

UsersInfosStats

<?php

namespace LCP\User\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * UsersInfosStats
 *
 * @ORM\Table(name="lcp_users_infos_stats")
 * @ORM\Entity
 */
class UsersInfosStats
{
    /**
     * @var integer
     * @ORM\Id
     * @ORM\OneToOne(targetEntity="LCP\User\UserBundle\Entity\UsersMain", cascade={"persist"}, inversedBy="infosStats")
     */
    private $id_user;

    /**
     * Set id_user
     *
     * @param \LCP\User\UserBundle\Entity\UsersMain $idUser
     * @return UsersInfosStats
     */
    public function setIdUser(\LCP\User\UserBundle\Entity\UsersMain $idUser)
    {
        $this->id_user = $idUser;

        return $this;
    }

    /**
     * Get id_user
     *
     * @return \LCP\User\UserBundle\Entity\UsersMain 
     */
    public function getIdUser()
    {
        return $this->id_user;
    }
}

Here is how i create a user (it is through a secured admin area form) :

/**
* This action get the field from the form
*/
public function userSaveAction(Request $request)
{
    $userManager = $this->get('fos_user.user_manager');
    $this->request = $request;
    $userValues = $this->getRequest()->request->all();
    $this->em = $this->getDoctrine()->getManager();

    $user = $this->processUser($userValues);

    $userManager->updateUser($user, false);

    return $this->redirect($this->generateUrl('lcp_admin_user_edit', ['id'=>$user->getId()]));
}

/**
* This method process user fields and uses setters (or getters for joined entities) to set datas of user
*/
private function processUser($userValues)
{
    if($userValues['userId']=="") {
        $user = $this->get('fos_user.user_manager')->createUser();
    } else {
        $user = $this->em->getRepository('LCPUserBundle:UsersMain')
                ->findOneBy(['id' => $userId]);
    }

    //construction of related entities
    $user->setInfosAdmin();
    $user->setInfosBank();
    $user->setInfosStats();
    $user->setApodis();

    unset($userValues['userId']);

    //this method inspect submitted field to check if they are mandatory or if current user is allowed to modify them
    $this->fields = $this->getInfosFields();

    foreach($userValues as $input=>$value){
        list($entity,$input)=explode('-',$input);
        if($input=='plainPassword' && $value==''){
            continue;
        }
        if($entity=='Main'){
            $setter = 'set'.ucfirst($input);
            $user->$setter($value);
        } else {
            $setter = 'set'.ucfirst($input);
            $join = 'get'.$entity;

            $user->$join()->$setter($value);
        }
    }
    return $user;
}
Philippe CARLE
  • 549
  • 3
  • 18

1 Answers1

0

According to the comments your UsersMain property need to be like this:

/**
 * Join administrative informations
 * @ORM\OneToOne(targetEntity="MyVendor\User\UserBundle\Entity\UsersInfosAdmin", mappedBy="userMain", cascade={"persist"}, orphanRemoval=true)
 */
private $infosAdmin;

/**
 * Setter InfosAdmin
 */
public function setInfosAdmin(UsersInfosAdmin $infosAdmin)
{
    $this->infosAdmin = $infosAdmin;
    $this->infosAdmin->setUserMain($this); <--- add this to set userMain on associated object

    return $this;
}

then your UsersInfosAdmin properties need to be like this:

/**
 * @var integer
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
 private $id;

/**
 * @ORM\OneToOne(targetEntity="MyVendor\User\UserBundle\Entity\UsersMain", inversedBy="userMain")
 */
 private $userMain;
gp_sflover
  • 3,460
  • 5
  • 38
  • 48
  • I was maybe not as clear as I thought ;) I'm talking about only one user type, but in my case, a user has ~80 fields… And what i'm talking about is 4 tables and 4 entities. One main entity for the main table, which has 3 associations for 3 complementary tables. – Philippe CARLE Dec 10 '14 at 16:46
  • Ah ok! I saw class UsersMain extends BaseUser (maybe you are using FosUserBundle). I will update my answer in a few minutes. – gp_sflover Dec 10 '14 at 16:52
  • @PhilippeC. remember to set user_id on each association setter. Let me know if that was the problem otherwise you will need to post all the relevant code. – gp_sflover Dec 10 '14 at 17:19
  • Still have the same problem… Doctrine doesn't want at all to insert main user and get last insert id before inserting the 3 other related entities… "Entity of type MyVendor\User\UserBundle\Entity\UsersInfosBank has identity through a foreign entity MyVendor\User\UserBundle\Entity\UsersMain, however this entity has no identity itself. You have to call EntityManager#persist() on the related entity and make sure that an identifier was generated before trying to persist 'MyVendor\User\UserBundle\Entity\UsersInfosBank'." – Philippe CARLE Dec 11 '14 at 08:43
  • Updates your question inserting all the code-logic related to the problem (specially where you manages the insertion). – gp_sflover Dec 11 '14 at 08:49
  • I saw just now that you've created the association using the annotation on the id property of the association! It's WRONG, You must create the association by creating (as example) a 'InfosAdminBank' property and adding on this the association annotation. PS: the 'userID' field will be automatically created by doctrine in the associated table. If you don't understand well I will update my answer in a few minutes... – gp_sflover Dec 11 '14 at 09:06
  • Ok, a huge thanks for your help, but after a lot of different attempt to make it work your way (which sounds good btw), and after having fixed all exceptions thrown, the only thing i could get was to be disconnected and unable to connect anymore due to a "system error"… I think i'm gonna go back to one huge table with 80 fields !! Btw, I can't figure out how doctrine can know which is the field to join other entities in its query as the mappedBy attribute points to a field which doens't physically exists in db… – Philippe CARLE Dec 11 '14 at 09:55