This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
04.

Class Table Inheritance

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to let's dive into the final type of Doctrine Inheritance: Class Table Inheritance. This one goes back to using a single table per entity in the hierarchy. But here's the difference - only properties specific to that entity are stored in the table (plus the id). When fetching, Doctrine does the necessary joins behind the scenes to get all the data for the entity you're fetching.

Over in starship, in the InheritanceType attribute, change SINGLE_TABLE to JOINED:

116 lines | src/Entity/Starship.php
// ... lines 1 - 8
#[ORM\InheritanceType('JOINED')]
// ... lines 10 - 14
abstract class Starship
// ... lines 16 - 116

Doctrine traditionally used JOINED to refer to this type of inheritance, but it's more commonly known as Class Table Inheritance. That's it!

Note

While this code change is super easy, database changes are not. If you had an already established database with data in it, you'd need to do some careful migrations.

To better show how the SQL changes, let's drop the schema and start fresh. Over in the terminal, run

symfony console doctrine:schema:drop --force

Now, create the schema with:

symfony console doctrine:schema:create --dump-sql

Observing the Changes

Let's see what we've got here. The freighter table just has an id and cargo_capacity. The starship table has all the common properties, and the scout table has just an id and sensor_range.

Let's load our fixtures to ensure everything still works.

symfony console foundry:load-fixtures

Looks good. Jump over to the app and refresh. Perfect! We still have six ships!

Checking the Database

Now, let's peek into the database to see what we're dealing with. Back in the terminal, run:

symfony console doctrine:query:sql 'select * from starship'

Note

Remember, this command has changed to dbal:run-sql in newer versions of Doctrine.

As you can see, this only contains the columns for the Starship entity, not Freighter and Scout. However, it still does include a ship_type. And yep, we have three freighters and three scouts. If we want to see the specific properties for these, they are in their respective tables.

Let's look at the freighter table. First, notice the id column for the freighters, 1, 2, and 3. These should match up with the ids in the freighter table. Run:

symfony console doctrine:query:sql 'select * from freighter'

This just has the cargo_capacity's for the three freighters, and sure enough, the ids match up with the ids in the starship table. So, when Doctrine is building (or hydrating) a starship, it knows that the ship with id 1 is a freighter. It then instantiates a Freighter object, grabs the common properties from the starship table, joins with the freighter table on the id, and grabs the cargo capacity. Phew, I'm glad Doctrine is doing all that for us!

Profiler Inspection

Jumping back to the app, let's take a moment to check the profiler and see this in action. Click the formatted query to get a better view. The query is a bit more complex than before, because of the joins, but it's doing exactly what we expected.

Adding a New Entity

Let's up the ante with a slightly more advanced scenario. We're going to create another ship - a mining freighter. It will be a subclass of Freighter, taking us another level deep in the inheritance tree.

Back in the terminal, create the entity with:

symfony console make:entity MiningFreighter

Give it one property called laserPower, an integer, not nullable, and done. Now the Foundry factory:

symfony console make:factory

Choose all to create the missing MiningFreighter.

Back in our IDE, open up our new MiningFreighter entity. Have it extend Freighter and remove the id property and getter:

26 lines | src/Entity/MiningFreighter.php
// ... lines 1 - 7
#[ORM\Entity(repositoryClass: MiningFreighterRepository::class)]
class MiningFreighter extends Freighter
// ... lines 10 - 26

Updating the Factory

Now, open the MiningFreighterFactory. We need to make a few adjustments to the FreighterFactory before we can extend it, so open up that.

Remove the final so we can extend it. Next, we need to add a template so sub-factories can specify the type of entity they are creating. In the class docblock, add @template T of Freighter. Then, in the @extends, scope it to T:

27 lines | src/Factory/FreighterFactory.php
// ... lines 1 - 6
/**
* @template T of Freighter
* @extends StarshipFactory<T>
*/
class FreighterFactory extends StarshipFactory
// ... lines 12 - 27

Back in MiningFreighterFactory, have the class extend FreighterFactory. Do the same for @extends in the docblock:

27 lines | src/Factory/MiningFreighterFactory.php
// ... lines 1 - 7
/**
* @extends FreighterFactory<MiningFreighter>
*/
final class MiningFreighterFactory extends FreighterFactory
// ... lines 12 - 27

Down in defatuls(), add our array_merge trick. array_merge(parent::defaults(), second argument, the array, and don't forget to close the function:

27 lines | src/Factory/MiningFreighterFactory.php
// ... lines 1 - 10
final class MiningFreighterFactory extends FreighterFactory
{
// ... lines 13 - 18
#[\Override]
protected function defaults(): array
{
return array_merge(parent::defaults(), [
'laserPower' => self::faker()->randomNumber(),
]);
}
}

We have some cool array_merge inception going on here. The MiningFreighterFactory is merging with the defaults from the FreighterFactory, which is merging with the defaults from StarshipFactory.

Creating Some Data

Next, over in AppStory, create a few mining freighters with MiningFreighterFactory::createMany(2):

21 lines | src/Story/AppStory.php
// ... lines 1 - 11
final class AppStory extends Story
{
public function build(): void
{
// ... lines 16 - 17
MiningFreighterFactory::createMany(2);
}
}

Back in the terminal, reload the fixtures.

symfony console foundry:load-fixtures

Oops, we have an error... "Entity MiningFreighter has to be part of the discriminator map of Starship to be properly mapped in the inheritance hierarchy."

This is a common step to forget when adding new entities to an inheritance hierarchy.

Back in Starship, in the DiscriminatorMap array, add our new entity with 'mining_freighter' => MiningFreighter::class:

117 lines | src/Entity/Starship.php
// ... lines 1 - 10
#[ORM\DiscriminatorMap([
// ... lines 12 - 13
'mining_freighter' => MiningFreighter::class,
])]
abstract class Starship
// ... lines 17 - 117

I like to use snake case for the keys, but you can use whatever you want. The important thing is that the value is the class name of the entity.

Now load our fixtures again.

symfony console foundry:load-fixtures

Nice, that worked! Refresh our app... and voila! We now have eight ships!

Conclusion

Take a peek at the query in the profiler again. Another join was added for the mining_freighter table.

This is one of the cons of class table inheritance, queries get more complex, the more entities you have in your hierarchy. This can lead to reduced performance.

Another con you may not think of is, if you use external tools to query and manipulate your database, it's more difficult to work with. You have to duplicate the joins and logic Doctrine uses internally.

Like most things, there are trade-offs with each type of inheritance.

Next, we'll look at how to query the different types of starships.