Jump to content

Stupid mistake with Doctrine entities and inheritance


NotionCommotion

Recommended Posts

I am doing something stupid, and just can't see it and hoping someone has better eyes than mine.  I have two Doctrine entities which extend another class using single class inheritance, but when retrieving the collection of either of the two child classes, I get the collection of the parent (i.e. combination of both child classes).  I've experimented with making the parent abstract and not including it in the discriminator map but no change.  In hopes to identifying my mistake, I created a new Symfony project with just the relevant classes and show all steps below.

Initial install by running the following:

symfony new test
composer update
composer require doctrine/orm
composer require migrations
composer require maker --dev
composer require security

Using "php bin/console make:user", I created three users: AbstractUser, OwnerUser, VendorUser.  I then edited all three entities and the owner and vendor repositories as follows:

<?php

namespace App\Entity;

use App\Repository\AbstractUserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass=AbstractUserRepository::class)
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap({"owner" = "OwnerUser", "vendor" = "VendorUser", "abstract" = "AbstractUser"})
 */
class AbstractUser implements UserInterface
{
    // No changes made
}

 

<?php

namespace App\Entity;

use App\Repository\OwnerUserRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=OwnerUserRepository::class)
 */
class OwnerUser extends AbstractUser
{
}

 

<?php

namespace App\Entity;

use App\Repository\VendorUserRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=VendorUserRepository::class)
 */
class VendorUser extends AbstractUser
{
}

No change to AbstractUserRepository.

<?php

namespace App\Repository;

class OwnerUserRepository extends AbstractUserRepository
{
}

 

<?php

namespace App\Repository;

class VendorUserRepository extends AbstractUserRepository
{
}

Then I migrated the DB.

php bin/console doctrine:schema:drop --full-database --force
rm migrations/*
php bin/console make:migration
php bin/console doctrine:migrations:migrate

Next I created two OwnerUsers and one VendorUser using a command script I wrote shown at the end of this post.

# php bin/console app:tester create OwnerUser roles=[]
# php bin/console app:tester create OwnerUser roles=[]
# php bin/console app:tester create VendorUser roles=[]

Checked the PostSQL database.

facdocs=> \d
                     List of relations
 Schema |            Name             |   Type   |  Owner
--------+-----------------------------+----------+---------
 public | abstract_user               | table    | facdocs
 public | abstract_user_id_seq        | sequence | facdocs
 public | doctrine_migration_versions | table    | facdocs
(3 rows)

facdocs=> select * from abstract_user;
 id |    email    | roles |    password    | discr
----+-------------+-------+----------------+--------
  1 | setEmail158 | []    | setPassword146 | owner
  2 | setEmail87  | []    | setPassword101 | owner
  3 | setEmail62  | []    | setPassword20  | vendor
(3 rows)

facdocs=>

Next, I retrieved the entities using Doctrine along with the beforementioned test script.
 

[michael@devserver test]$ php bin/console app:tester read VendorUser
Test Entity Manager
============

command: read
entity: VendorUser
class: \App\Entity\VendorUser
properties: []
array(3) {
  [0]=>
  object(stdClass)#293 (5) {
    ["__CLASS__"]=>
    string(20) "App\Entity\OwnerUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(1)
    ["email:App\Entity\AbstractUser:private"]=>
    string(11) "setEmail158"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(14) "setPassword146"
  }
  [1]=>
  object(stdClass)#294 (5) {
    ["__CLASS__"]=>
    string(20) "App\Entity\OwnerUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(2)
    ["email:App\Entity\AbstractUser:private"]=>
    string(10) "setEmail87"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(14) "setPassword101"
  }
  [2]=>
  object(stdClass)#358 (5) {
    ["__CLASS__"]=>
    string(21) "App\Entity\VendorUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(3)
    ["email:App\Entity\AbstractUser:private"]=>
    string(10) "setEmail62"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(13) "setPassword20"
  }
}
[michael@devserver test]$
[michael@devserver test]$ php bin/console app:tester read OwnerUser
Test Entity Manager
============

command: read
entity: OwnerUser
class: \App\Entity\OwnerUser
properties: []
array(3) {
  [0]=>
  object(stdClass)#293 (5) {
    ["__CLASS__"]=>
    string(20) "App\Entity\OwnerUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(1)
    ["email:App\Entity\AbstractUser:private"]=>
    string(11) "setEmail158"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(14) "setPassword146"
  }
  [1]=>
  object(stdClass)#294 (5) {
    ["__CLASS__"]=>
    string(20) "App\Entity\OwnerUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(2)
    ["email:App\Entity\AbstractUser:private"]=>
    string(10) "setEmail87"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(14) "setPassword101"
  }
  [2]=>
  object(stdClass)#358 (5) {
    ["__CLASS__"]=>
    string(21) "App\Entity\VendorUser"
    ["id:App\Entity\AbstractUser:private"]=>
    int(3)
    ["email:App\Entity\AbstractUser:private"]=>
    string(10) "setEmail62"
    ["roles:App\Entity\AbstractUser:private"]=>
    string(8) "Array(0)"
    ["password:App\Entity\AbstractUser:private"]=>
    string(13) "setPassword20"
  }
}
[michael@devserver test]$

 

As seen, when requesting a collection of either OwnerUsers or VendorUsers, I am retrieving the combination of both collections.  I am sure it is totally user (i.e. me) error, but just don't see it.  Are you able to see my stupid mistake?  Thanks


My test script is below:
 

<?php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Common\Util\Debug;

class Tester extends Command
{
    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'app:tester';
    private $entityManager;
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        parent::__construct();
    }

    protected function configure()
    {
        $this
        ->setDescription('Doctrine Object Tester.  --help')
        ->setHelp('This command allows you to query a single entity...')
        ->addArgument('cmd', InputArgument::REQUIRED, 'The command name [create, read].')
        ->addArgument('entity', InputArgument::REQUIRED, 'The name of the entity.')
        ->addArgument('properties', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'properies (foo=foo bla=[]')
        ;
    }

    private function getProperties($input)
    {
        $properties=[];
        foreach($input as $property) {
            $property=explode('=', $property);
            $name = trim($property[0]);
            $value = trim($property[1]);
            if($value === '[]') {
                $value = [];
            }
            elseif($value === 'null') {
                $value = null;
            }
            elseif(substr($value, 0, 1) === '/') {
                $value = explode('/', substr($value, 1));
                $class = ucfirst(substr($value[0], 0, -1));
                $class = '\App\Entity\\'.$class;
                if(count($value)===1) {
                    $value = $this->entityManager->getRepository($class)->findAll();
                }
                else {
                    $value = $this->entityManager->getRepository($class)->find($value[1]);
                }
            }
            $properties[$name] = $value;
        }
        return $properties;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln([
            'Test Entity Manager',
            '============',
            '',
        ]);

        // retrieve the argument value using getArgument()

        $cmd = $input->getArgument('cmd');
        $output->writeln('command: '.$cmd);
        $entity = $input->getArgument('entity');
        $output->writeln('entity: '.$entity);
        $class = '\App\Entity\\'.$entity;
        $output->writeln('class: '.$class);
        $properties = $this->getProperties($input->getArgument('properties'));

        $output->writeln('properties: '.json_encode($properties));

        switch($cmd) {
            case 'create':
                $entity = new $class;
                foreach(get_class_methods($entity) as $method) {
                    if(substr($method, 0, 3)==='set') {
                        $m = lcfirst(substr($method, 3));
                        $entity->$method(array_key_exists($m, $properties)?$properties[$m]:$method.rand(0,200));
                    }
                }
                $this->entityManager->persist($entity);
                $this->entityManager->flush();
                $output->writeln('entity created');
                break;
            case 'read':
                Debug::dump($this->entityManager->getRepository($class)->findAll());
                break;
            default:
                $output->writeln('INVALID COMMAND: '.$cmd.'.  Only create and read are supported.'); 
        }
        return Command::SUCCESS;
    }
}

 

Link to comment
Share on other sites

I have no idea why this would make a different, but if I remove the repository declaration in the child entity, I now get the desired results.  Of course, now I can't have separate repositories for each sub-class...


Does not work:

/**
 * @ORM\Entity
 * @ORM\Entity(repositoryClass=OwnerUserRepository::class)
 */
class OwnerUser extends AbstractUser
{
}

Works:

/**
 * @ORM\Entity
 */
class OwnerUser extends AbstractUser
{
}


 

Link to comment
Share on other sites

On 2/2/2021 at 6:48 AM, kicken said:

Just a guess but what if you don't have your repositories extend AbstractUserRepository and instead just be their own separate repositories?

Thanks kicken.  I half hearted tested your suggestion with no expectations, and behold you are absolutely correct!  Then, I was more confused regarding why it worked, but then spotted my stupid mistake.  Previously I've always manually created my Doctrine repositories and added a 'create()` method to each to create an entity.  Symfony does almost the same but injects the class name of the to be created class in the constructor.  Happy to find that it was nothing magical but just normal PHP.  My final solution still extends AbstractUserRepository, but each entity has its own constructor.  Thanks again!

<?php

namespace App\Repository;

use App\Entity\Vendor;
use Doctrine\Persistence\ManagerRegistry;

class VendorRepository extends AbstractUserRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Vendor::class);
    }
}

 

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.