OiO.lk Blog PHP ORM Mapping different type of users
PHP

ORM Mapping different type of users


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

Exit mobile version