I have an app with different types of users. All type of users share common properties like username, firstname and lastname. The different type of users are:
- Child
- Guardian
- Teacher
Teacher has all the properties of user, but also an email and phone number. Idem for child and guardian, but there is a many-to-many relationship between these two entities. A child can have multiple parents, and a parent can have multiple children.
I designed my models to have a parent-class User
with the common properties, and I have a class for each type as subclass. The idea was that it’d generate database tables Users, Children, Guardians, and Teachers. Where the last three would have a FK to Users so when I retrieve e.g a Teacher I can also retrieve it’s first- and lastname.
These are my models:
User.php
<?php
namespace App\Domain\Model\User;
use App\Domain\Model\Timestampable;
use App\Domain\Model\HasUuid;
class User
{
use Timestampable;
use HasUuid;
public function __construct(
private string $username,
private string $firstName,
private string $lastName,
private string $passcode,
)
{
$this->username = $username;
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->passcode = $passcode;
}
public function getUsername(): string
{
return $this->username;
}
public function setUsername(string $username): void
{
$this->username = $username;
}
public function getFirstName(): string
{
return $this->firstName;
}
public function setFirstName(string $firstName): void
{
$this->firstName = $firstName;
}
public function getLastName(): string
{
return $this->lastName;
}
public function setLastName(string $lastName): void
{
$this->lastName = $lastName;
}
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
public function getPasscode(): string
{
return $this->passcode;
}
public function setPasscode(string $passcode): void
{
$this->passcode = $passcode;
}
}
Child.php
<?php
namespace App\Domain\Model\User;
use App\Domain\Model\HasUuid;
use App\Domain\Model\User\User;
use App\Domain\Model\User\Guardian;
class Child extends User
{
use HasUuid;
public function __construct(
private string $username,
private string $firstName,
private string $lastName,
private string $passcode,
private array $guardians,
)
{
parent::__construct($username, $firstName, $lastName, $passcode);
}
public function getGuardians(): array
{
return $this->guardians;
}
public function addGuardian(Guardian $guardian): void
{
$this->guardians[] = $guardian;
}
}
Guardian.php
<?php
namespace App\Domain\Model\User;
use App\Domain\Model\HasUuid;
use App\Domain\Model\User\User;
use App\Domain\Model\User\Child;
class Guardian extends User
{
use HasUuid;
public function __construct(
private string $username,
private string $firstName,
private string $lastName,
private string $passcode,
private string $email,
private string $telnr,
private array $children,
)
{
parent::__construct($username, $firstName, $lastName, $passcode);
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getTelnr(): string
{
return $this->telnr;
}
public function setTelnr(string $telnr): void
{
$this->telnr = $telnr;
}
public function getChildren(): array
{
return $this->children;
}
public function addChild(Child $child): void
{
$this->children[] = $child;
}
}
Teacher.php
<?php
namespace App\Domain\Model\User;
use App\Domain\Model\Timestampable;
use App\Domain\Model\HasUuid;
use App\Domain\Model\User\User;
class Teacher extends User
{
use HasUuid;
public function __construct(
private string $username,
private string $firstName,
private string $lastName,
private string $passcode,
private string $email,
private string $telnr,
)
{
parent::__construct($username, $firstName, $lastName, $passcode);
$this->email = $email;
$this->telnr = $telnr;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getTelnr(): string
{
return $this->telnr;
}
public function setTelnr(string $telnr): void
{
$this->telnr = $telnr;
}
}
Now I am using Doctrine/ORM in XML format to generate the database scheme. This is my current config:
User.orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Domain\Model\User\User" table="users" inheritance-type="JOINED">
<id name="id" type="guid" column="id">
<generator strategy="NONE"/>
</id>
<field name="username" type="string" length="255" nullable="false"/>
<field name="firstName" type="string" length="255" nullable="false"/>
<field name="lastName" type="string" length="255" nullable="false"/>
<field name="passcode" type="string" length="255" nullable="false"/>
<field name="createdAt" type="datetime" nullable="false"/>
<field name="updatedAt" type="datetime" nullable="false"/>
<field name="deletedAt" type="datetime" nullable="true"/>
<discriminator-column name="type" type="string" />
<discriminator-map>
<discriminator-mapping value="teacher" class="App\Domain\Model\User\Teacher" />
<discriminator-mapping value="guardian" class="App\Domain\Model\User\Guardian" />
<discriminator-mapping value="child" class="App\Domain\Model\User\Child" />
</discriminator-map>
</entity>
</doctrine-mapping>
Child.orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Domain\Model\User\Child" table="children">
<!-- Many-to-many relationship with Guardian -->
<many-to-many field="guardians" target-entity="App\Domain\Model\User\Guardian">
<join-table name="guardians_children">
<join-columns>
<join-column name="child_id" referenced-column-name="id" nullable="false"/>
</join-columns>
<inverse-join-columns>
<join-column name="guardian_id" referenced-column-name="id" nullable="false"/> <!-- Ensure NOT NULL -->
</inverse-join-columns>
</join-table>
</many-to-many>
</entity>
</doctrine-mapping>
Guardian.orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Domain\Model\User\Guardian" table="guardians">
<!-- Many-to-many relationship with Child -->
<many-to-many field="children" target-entity="App\Domain\Model\User\Child" inversed-by="guardians">
<join-table name="guardian_child">
<join-columns>
<join-column name="guardian_id" referenced-column-name="id" nullable="false"/> <!-- Ensure NOT NULL -->
</join-columns>
<inverse-join-columns>
<join-column name="child_id" referenced-column-name="id" nullable="false"/> <!-- Ensure NOT NULL -->
</inverse-join-columns>
</join-table>
</many-to-many>
</entity>
</doctrine-mapping>
Teacher.orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Domain\Model\User\Teacher" table="teachers">
<field name="email" type="string" length="255" nullable="false"/>
<field name="telnr" type="string" length="50" nullable="false"/>
</entity>
</doctrine-mapping>
Generating a migration from this creates the following tables:
[
As you can see the tables contain the correct fields and user contains a discriminator to determine the type of user. But the tables representing the different types of users are missing something. When I would retrieve a teacher in this database I would have no idea what user it actually is and it would be impossible to get the firstname, lastname,… because the table Teachers does not have a FK to the table Users.
I thought I could solve this by adding a many-to-one in the sub-tables in xml:
<entity name="App\Domain\Model\User\Teacher" table="teachers">
<field name="email" type="string" length="255" nullable="false"/>
<field name="telnr" type="string" length="50" nullable="false"/>
<many-to-one field="user" target-entity="App\Domain\Model\User\User" inversed-by="teachers">
<join-column name="id" referenced-column-name="id" nullable="false"/>
</many-to-one>
</entity>
But this gives the following error:
Property App\Domain\Model\User\Teacher::$user does not exist
This could be solved by adding a property $user
to the Teacher-class. But Teacher is already extending from User so that does not really seem to make sense to me.
I think I am doing something wrong, how do I successfully implement different type of users?
You need to sign in to view this answers
Leave feedback about this