Multiple class inheritance with Doctrine ODM in Symfony: One collection for multiple document types

Eneko
enekochan
Published in
5 min readJan 10, 2016

When you use Doctrine ORM in Symfony all the data is stored in tables having every row the same columns. Storing different type of entities in the same table is not posible (AFAIK). But MongoDB can store any kind of document in the same collection making possible to have different kind of objects in the same place and thus making inheritance easy to use if you need it.

In this example I’m going to create the documents needed to reflect an RFID system (from a real application I did sometime ago) where cards, called internally tag, can represent different things: a person, an item, an event, etc. Thus our mongodb database will have a collection of tags with documents of type person, item, event, etc.

Each time a card is used the system will know what it represents using the internal EPC of it: as you may be thinking this will be our ID.

Install MongoDB, the PHP driver and create a new database

This process depends on the operating system you use. Here are the instructions for some of them in older posts:

To create a new database in our mongodb server you have just to make a use statement and create a new collection in it with the mongo client:

> use app
switched to db app
> db.createCollection('tags')
{ "ok" : 1 }

Install MongoDB Doctrine library and DoctrineMongoDBBundle

This process is needed to be able to use MongoDB within the Symfony project. Those steps are extracted from the Symfony DoctrineMongoDBBundle Documentation.

Add the MongoDB Doctrine library and the DoctrineMongoDBBundle to composer.json:

{
"require": {
"doctrine/mongodb-odm": "~1.0",
"doctrine/mongodb-odm-bundle": "~3.0"
},
}

Now install them using composer. Using a local installation of it:

$ php composer.phar update doctrine/mongodb-odm doctrine/mongodb-odm-bundle

Or a global installation:

$ composer update doctrine/mongodb-odm doctrine/mongodb-odm-bundle

Register the annotations library by adding the following to the autoloader (below the existing AnnotationRegistry::registerLoader line):

use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
AnnotationDriver::registerAnnotationClasses();

It should look something like this:

<?php
// app/autoload.php
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
use Composer\Autoload\ClassLoader;

/**
* @var ClassLoader $loader
*/
$loader = require __DIR__.'/../vendor/autoload.php';

AnnotationRegistry::registerLoader(array($loader, 'loadClass'));
AnnotationDriver::registerAnnotationClasses();

return $loader;

Now update AppKernel.php:

// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle(),
);

// ...
}

Configure MongoDB ODM

Symfony must know how to connect to our mongodb instance. This is configured in app/config/config.yml being the easiest way using the auto_mapping option:

# app/config/config.yml
doctrine_mongodb:
connections:
default:
server: mongodb://localhost:27017
options: {}
default_database: test_database
document_managers:
default:
auto_mapping: true

I like using the app/config/parameters.yml to set up the host, port and database name:

# app/config/parameters.yml
parameters:
mongodb_host: 127.0.0.1
mongodb_port: 27017
mongodb_name: app
# app/config/config.yml
imports:
- { resource: parameters.yml }

doctrine_mongodb:
connections:
default:
server: mongodb://%mongodb_host%:%mongodb_port%
options: {}
default_database: %mongodb_name%
document_managers:
default:
auto_mapping: true

The document classes

Now is when we start coding. I’ll suppose you already have an AppBundle created (if not look here at the Symfony Best Practices — Creating the project).

The “parent” class: Single Collection Inheritance

All the different things we want to represent as a document in mongodb in the same collection and an object in PHP must inherit from the same class called tag. The "magic" here is the @MongoDB\InheritanceType("SINGLE_COLLECTION") annotation. This will tell MongoDB ODM that this collection will store different documents and a discrimination field will tell each other apart.

<?php
// src/AppBundle/Document/TagInterface.php

namespace AppBundle\Document;

interface TagInterface
{
}
<?php
// src/AppBundle/Document/Tag.php

namespace AppBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/**
* @MongoDB\Document(collection="tags")
* @MongoDB\InheritanceType("SINGLE_COLLECTION")
* @MongoDB\DiscriminatorField("type")
* @MongoDB\DiscriminatorMap({"person"="Person", "item"="Item"})
*/
class Tag implements TagInterface
{
/**
* @MongoDB\Id(strategy="NONE")
*/
protected $epc;

/**
* Set epc
*
* @param custom_id $epc
* @return self
*/
public function setEpc($epc)
{
$this->epc = $epc;
}

/**
* Get epc
*
* @return custom_id $epc
*/
public function getEpc()
{
return $this->epc;
}
}

The inherited classes: extends

Now that we have the parent class the only thing we need to store documents in that collection is extend it:

<?php
// src/AppBundle/Document/Person.php

namespace AppBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/**
* @MongoDB\Document
*/
class Person extends Tag
{
/**
* @MongoDB\String
*/
protected $name;

/**
* Set name
*
* @param string $name
* @return self
*/
public function setName($name)
{
$this->name = $name;

return $this;
}

/**
* Get name
*
* @return string $name
*/
public function getName()
{
return $this->name;
}
}
<?php
// src/AppBundle/Document/Item.php

namespace AppBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/**
* @MongoDB\Document
*/
class Item extends Tag
{
/**
* @MongoDB\Float
*/
protected $price;

/**
* Set price
*
* @param float $price
* @return self
*/
public function setPrice($price)
{
$this->price = $price;

return $this;
}

/**
* Get price
*
* @return float $price
*/
public function getPrice()
{
return $this->price;
}
}

Store and read documents to/from the collection

All the configuration and classes are done, we can start persisting and finding documents anywhere in the project: a controller, a command, etc. For example lets persist and find some Person and Item objects in a controller:

<?php
// src/AppBundle/Controller/TagsController.php

namespace AppBundle\Controller;

use AppBundle\Document\Item;
use AppBundle\Document\Person;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class TagsController extends Controller
{
/**
* @Route("/persist", name="app_persist")
* @param Request $request The Request object
* @return Response A Response instance
*/
public function persistAction()
{
$item = new Item();
$item->setEpc('0000000000000000000000AA');
$item->setPrice('19.99');

$person = new Person();
$item->setEpc('0000000000000000000000AB');
$item->setName('Eneko');

$dm = $this->get('doctrine_mongodb')->getManager();
$dm->persist($item);
$dm->persist($person);
$dm->flush();

return new Response('Created tags');
}

/**
* @Route("/find", name="app_find")
* @param Request $request The Request object
* @return Response A Response instance
*/
public function findAction(Request $request)
{
$epc = $request->request->get('epc');
$tag = $this->get('doctrine_mongodb')
->getRepository('AppBundle:Tag')
->find($epc);

if (!$tag) {
throw $this->createNotFoundException('No tag found for epc '.$epc);
}

switch(true) {
case $tag instanceof Person:
$type = 'Person';
break;
case $tag instanceof Item:
$type = 'Item';
break;
}

return new Response('Found '.$type);
}

/**
* @Route(
* "/find-with-param-converter/{epc}",
* requirements={
* "epc": "[a-zA-Z0-9]{24}"
* },
* name="app_find_with_param_converter"
* )
* @ParamConverter("tag", class="AppBundle\Document\Tag")
* @param Request $request The Request object
* @param TagInterface $tag A object of class Tag or one of its children
* @return Response A Response instance
*/
public function findWithParamConverterAction(Request $request, TagInterface $tag)
{
switch(true) {
case $tag instanceof Person:
$type = 'Person';
break;
case $tag instanceof Item:
$type = 'Item';
break;
}

return new Response('Found '.$type);
}
}

And how are all those object stored in mongodb and how can Doctrine know what class each one represents? That’s configured in the parent class with the @MongoDB\DiscriminatorField and the @MongoDB\DiscriminatorMap annotations. Each document simply has a field named type with the name of the class on it:

> db.tags.find()
{
"_id" : "0000000000000000000000AA",
"type" : "item",
"price" : 19.99
},
{
"_id" : "0000000000000000000000AB",
"type" : "person",
"name" : "Eneko"
}

Ref: http://doctrine-mongodb-odm.readthedocs.org/en/latest/reference/basic-mapping.html
http://doctrine-mongodb-odm.readthedocs.org/en/latest/reference/inheritance-mapping.html
http://jwage.com/post/30490180105/inheritance-and-mapped-super-classes-in-doctrine

--

--