Entities
While Laravel maps your Model
objects to tables using the ActiveRecord
pattern, Doctrine implements
the DataMapper
pattern to turn an object's fields into columns in a table. We'll use two patterns to
differentiate objects that can be persisted into a database: Entities and Embeddables.
What is an Entity
Acording to Eric Evans classification of domain objects, entities are:
Objects that have a distinct identity that runs through time and different representations. You also hear these called "reference objects".
With Doctrine, we'll map each Entity
to a table in our database, and each field we choose to persist
to (at least) one column in it.
Identity
Entities must have a distinct identity, which we usually refer to as ID. IDs can be autogenerated by our database or generated manually by us, and may be a single incremental integer or any other type of data, as long as it's unique across the table that holds these Entities.
<?php
namespace App\Entities\Research;
/**
* In doctrine, your Entities don't need to extend or implement anything.
* These are usually refered to as Plain Old PHP Objects (or POPOs).
* This means you are free to create your own types and relations, without Doctrine
* getting in your way.
*/
class Scientist
{
/**
* We'll need a field to hold our identity. We'll call it "id", but
* this could be called anything.
* Note that I'll make it private: Doctrine doesn't force this on us, we can
* choose the visibility on our fields.
*/
private $id;
// We don't need anything else! No setters or getters, this will work as is.
}
Now that we have an Entity
, we'll need to tell Doctrine how to map it to the database. For that, we'll
use our Fluent
mapper, but you could choose any other mapper available in Doctrine.
We'll create a separate class to map the Scientist
, and we'll call it ScientistMapping
just to keep a
convention. You could choose whatever name you like, so long as this class extends the EntityMapping
abstract class.
Internals fun-fact! This mapping driver needs you to implement a single interface:
LaravelDoctrine\Fluent\Mapping
. But one of the methods needed in that interface only differs between Entities, MappedSuperClasses and Embeddables, so to simplify its use, we made three abstract classes that implement that method and leave it up to you to implement the missing ones.Those abstract classes are:
LaravelDoctrine\Fluent\EntityMapping
,LaravelDoctrine\Fluent\EmbeddableMapping
andLaravelDoctrine\Fluent\MappedSuperClassMapping
.
The Mapping class
We'll need to tell doctrine which object are we mapping and how fields of this object map to columns.
<?php
namespace App\Mappings;
use App\Entities\Research\Scientist;
use LaravelDoctrine\Fluent\EntityMapping;
use LaravelDoctrine\Fluent\Fluent;
class ScientistMapping extends EntityMapping
{
/**
* Returns the fully qualified name of the class that this mapper maps.
*
* @return string
*/
public function mapFor()
{
// Here we tell Doctrine that this mapping is for the Scientist object.
return Scientist::class;
}
/**
* Load the object's metadata through the Metadata Builder object.
*
* @param Fluent $builder
*/
public function map(Fluent $builder)
{
/*
* Here we'll map each field in the object.
* Right now we'll just add the single "id" field as an "increments" type: that's our shortcut to
* tell Doctrine to do an auto-incrementing, unsigned, primary integer field.
* We could also do `bigIncrements('id')` or the whole `integer('id')->primary()->unsigned()->autoIncrement()`
*/
$builder->increments('id');
}
}
As you can see, we used the Fluent
builder to map a field to a specific type of column in the database. In our case,
the string 'id' represents the field that doctrine will use inside our entity. The column name will be built from the
field name through a NamingStrategy
, but we have you covered there! Laravel-doctrine ORM has the LaravelNamingStrategy
to keep using snake-cased singular columns and snake-cased plural tables, based on your objects fields and name,
respectively.
Adding the mapping class to the driver
If you're using Laravel and Doctrine through the laravel-doctrine/orm
package, then all you need to do here
is add the ScientistMapping
class reference to your config file:
return [
// ...
'managers' => [
'default' => [
'meta' => 'fluent',
'mappings' => [
App\Mappings\ScientistMapping::class,
],
// ...
];
In standalone mode (and that includes other frameworks by the time of this writing), you should have an instance
of the FluentDriver
created at boot / kernel time. The FluentDriver
object has an addMapping(Mapping $mapping)
method
that will accept an instance of your mapping, or an addMappings(string[] $mappings)
method that will construct them for
you and add them in bulk. Use any or both at your own convenience!
// either...
$driver = new FluentDriver([
App\Mappings\ScientistMapping::class,
]);
// or...
$driver = new FluentDriver;
$driver->addMapping(new ScientistMapping);
// or...
$driver = new FluentDriver;
$driver->addMappings([
App\Mappings\ScientistMapping::class,
]);
// Then tell Doctrine to use this mapping
$configuration->setMetadataDriverImpl($driver);
Adding more fields
We'll make this scientist more interesting: lets give it a name.
<?php
namespace App\Entities\Research;
class Scientist
{
/** @var int */
private $id;
/** @var string */
private $firstName;
/** @var string */
private $lastName;
/**
* @param string $firstName
* @param string $lastName
*/
public function __construct($firstName, $lastName)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
A few things to notice from this example:
- We added some DocBlocks to indicate the type we expect those fields to have. This is NOT mappings or annotations,
it is just a comment. The
Fluent
driver will NOT parse it. - We added a custom
__construct
method. UnlikeEloquent
, we are in complete control of our objects design, including its constructor. We'll be calling the constructor on an Entity the moment we create it for the first time, but when Doctrine fetches an already existing Entity from the database, it won't call its constructor to create it.
Now, lets map these new fields to the database and see how that looks:
<?php
namespace App\Mappings;
use App\Entities\Research\Scientist;
use LaravelDoctrine\Fluent\EntityMapping;
use LaravelDoctrine\Fluent\Fluent;
class ScientistMapping extends EntityMapping
{
/** @return string */
public function mapFor()
{
return Scientist::class;
}
/** @param Fluent $builder */
public function map(Fluent $builder)
{
$builder->increments('id');
/**
* Notice how we reference the field here, and not the column.
*/
$builder->string('firstName');
$builder->string('lastName');
}
}
As you can see, we added two strings to our mapping, one for each field on the Scientist. As in Laravel's migrations,
all fields are NOT NULL
by default. We've protected ourselves of that scenario with our __construct
, but maybe we
could add a more strict validation of those fields in the future.
If we run our schema update command...
$ php artisan doctrine:schema:update
...and do a quick seed of our first Scientist...
<?php
$al = new App\Entities\Research\Scientist('Albert', 'Einstein');
EntityManager::persist($al);
EntityManager::flush();
...this is how the database should look now:
| scientists |
| id integer | first_name varchar | last_name varchar |
|------------|--------------------|-------------------|
| 1 | "Albert" | "Einstein" |
We are being intentionally loose on words here. The specific schema will vary between DB engines, but that will be taken care of by Doctrine's DBAL layer, so don't worry about it.
By now you should be able to map your entities to a relational database. In the next chapters you'll learn how to map relations between them and how to improve your object's design with embedded objects and inheritance mapping.