1
0
mirror of synced 2025-02-20 14:13:15 +03:00

Huge refactoring of the Getting Started Tutorial to allow for an earlier success with the simple Product entity.

This commit is contained in:
Benjamin Eberlei 2013-03-17 13:43:16 +01:00
parent 4ef8e8c7aa
commit ea19a0063f

View File

@ -44,6 +44,19 @@ as a secondary problem. This doesn't mean persistence is downplayed by Doctrine
object-oriented programming if persistence and entities are kept
separated.
What are Entities?
~~~~~~~~~~~~~~~~~~
Entities are PHP Objects that can be identified over many requests
by a unique identifier or primary key. These classes don't need to extend any
abstract base class or interface. An entity class must not be final
or contain final methods. Additionally it must not implement
**clone** nor **wakeup** or :doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
An entity contains persistable properties. A persistable property
is an instance variable of the entity that is saved into and retrieved from the database
by Doctrine's data mapping capabilities.
An Example Model: Bug Tracker
-----------------------------
@ -78,7 +91,7 @@ the following contents:
"symfony/yaml": "2.*"
},
"autoload": {
"psr-0": {"": "entities"}
"psr-0": {"": "src/"}
}
}
@ -103,39 +116,115 @@ You can prepare the directory structure:
| `-- yaml
`-- src
A first prototype
-----------------
Obtaining the EntityManager
---------------------------
We start with a simplified design for the bug tracker domain, by creating three
classes ``Bug``, ``Product`` and ``User`` and putting them into
`src/Bug.php`, `src/Product.php` and `src/User.php`
respectively. We want instances of this three classes to be saved
in the database. A class saved into a database with Doctrine is called
entity. Entity classes are part of the domain model of your application.
Doctrine's public interface is the EntityManager, it provides the
access point to the complete lifecycle management of your entities
and transforms entities from and back to persistence. You have to
configure and create it to use your entities with Doctrine 2. I
will show the configuration steps and then discuss them step by
step:
.. code-block:: php
<?php
// src/Bug.php
class Bug
{
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $description;
/**
* @var DateTime
*/
protected $created;
/**
* @var string
*/
protected $status;
}
// bootstrap.php
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
require_once "vendor/autoload.php";
// Create a simple "default" Doctrine ORM configuration for Annotations
$isDevMode = true;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/entities"), $isDevMode);
// or if you prefer yaml or XML
//$config = Setup::createXMLMetadataConfiguration(array(__DIR__."/config/xml"), $isDevMode);
//$config = Setup::createYAMLMetadataConfiguration(array(__DIR__."/config/yaml"), $isDevMode);
// database configuration parameters
$conn = array(
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/db.sqlite',
);
// obtaining the entity manager
$entityManager = EntityManager::create($conn, $config);
The first require statement sets up the autoloading capabilities of Doctrine
using the Composer autoload.
The second block consists of the instantiation of the ORM
``Configuration`` object using the Setup helper. It assumes a bunch
of defaults that you don't have to bother about for now. You can
read up on the configuration details in the
:doc:`reference chapter on configuration <../reference/configuration>`.
The third block shows the configuration options required to connect
to a database, in my case a file-based sqlite database. All the
configuration options for all the shipped drivers are given in the
`DBAL Configuration section of the manual <http://www.doctrine-project.org/documentation/manual/2_0/en/dbal>`_.
The last block shows how the ``EntityManager`` is obtained from a
factory method.
Generating the Database Schema
------------------------------
Now that we have defined the Metadata Mappings and bootstrapped the
EntityManager we want to generate the relational database schema
from it. Doctrine has a Command-Line-Interface that allows you to
access the SchemaTool, a component that generates the required
tables to work with the metadata.
For the command-line tool to work a cli-config.php file has to be
present in the project root directory, where you will execute the
doctrine command. Its a fairly simple file:
.. code-block:: php
<?php
// cli-config.php
require_once "bootstrap.php";
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($entityManager)
));
You can then change into your project directory and call the
Doctrine command-line tool:
::
$ cd project/
$ php vendor/bin/doctrine orm:schema-tool:create
During the development you probably need to re-create the database
several times when changing the Entity metadata. You can then
either re-create the database:
::
$ php vendor/bin/doctrine orm:schema-tool:drop --force
$ php vendor/bin/doctrine orm:schema-tool:create
Or use the update functionality:
::
$ php vendor/bin/doctrine orm:schema-tool:update --force
The updating of databases uses a Diff Algorithm for a given
Database Schema, a cornerstone of the ``Doctrine\DBAL`` package,
which can even be used without the Doctrine ORM package. However
its not available in SQLite since it does not support ALTER TABLE.
Starting with the Product
-------------------------
We start with the Product entity requirements, because it is the most simple one
to get started. Create a ``src/Product.php`` file and put the ``Product``
entity definition in there:
.. code-block:: php
@ -168,6 +257,196 @@ entity. Entity classes are part of the domain model of your application.
}
}
Note how the properties have getter and setter methods defined except
`$id`. To access data from entities Doctrine 2 uses the Reflection API, so it
is possible for Doctrine to access the value of `$id`. You don't have to
take Doctrine into account when designing access to the state of your objects.
The next step for persistence with Doctrine is to describe the
structure of the ``Product`` entity to Doctrine using a metadata
language. The metadata language describes how entities, their
properties and references should be persisted and what constraints
should be applied to them.
Metadata for entities are configured using a
XML, YAML or Docblock Annotations. This
Getting Started Guide will show the mappings for all Mapping Drivers.
References in the text will be made to the XML mapping.
.. configuration-block::
.. code-block:: php
<?php
// src/Product.php
/**
* @Entity @Table(name="products")
**/
class Product
{
/** @Id @Column(type="integer") @GeneratedValue **/
protected $id;
/** @Column(type="string") **/
protected $name;
// .. (other code)
}
.. code-block:: xml
<!-- config/xml/Product.dcm.xml -->
<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://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
<entity name="Product" table="products">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="name" type="string" />
</entity>
</doctrine-mapping>
.. code-block:: yaml
# config/yaml/Product.dcm.yml
Product:
type: entity
table: products
id:
id:
type: integer
generator:
strategy: AUTO
fields:
name:
type: string
The top-level ``entity`` definition tag specifies information about
the class and table-name. The primitive type ``Product::$name`` is
defined as ``field`` attributes. The Id property is defined with
the ``id`` tag. The id has a ``generator`` tag nested inside which
defines that the primary key generation mechanism automatically
uses the database platforms native id generation strategy, for
example AUTO INCREMENT in the case of MySql or Sequences in the
case of PostgreSql and Oracle.
You have to update the database now, because we have a first Entity now:
::
$ php vendor/bin/doctrine orm:schema-tool:update
Now create a simple script to create a new product:
.. code-block:: php
<?php
// create_product.php
require_once "bootstrap.php";
$newProductName = $argv[1];
$product = new Product();
$product->setName($newProductName);
$entityManager->persist($product);
$entityManager->flush();
echo "Created Product with ID " . $product->getId() . "\n";
Call this script to see how new products are created:
::
$ php create_product.php ORM
$ php create_product.php DBAL
What is happening here? In the code using the Product is pretty standard OOP.
The interesting bits are the communication with the ``EntityManager``. To
notify the EntityManager that a new entity should be inserted into the database
you have to call ``persist()``. However the EntityManager does not act on this
command, its merely notified. You have to explicitly call ``flush()`` to have
the EntityManager write those two entities to the database.
You might wonder why does this distinction between persist notification and
flush exist: Doctrine 2 uses the UnitOfWork pattern to aggregate all writes
(INSERT, UPDATE, DELETE) into one single transaction, which is executed when
flush is called. Using this approach the write-performance is significantly
better than in a scenario where updates are done for each entity in isolation.
In more complex scenarios than the previous two, you are free to request
updates on many different entities and all flush them at once.
Doctrine's UnitOfWork detects entities that have changed after retrieval from
the database automatically when the flush operation is called, so that you only
have to keep track of those entities that are new or to be removed and pass
them to ``EntityManager#persist()`` and ``EntityManager#remove()``
respectively.
We want to see a list of all the products now, so lets create a new script for
this:
.. code-block:: php
<?php
// list_products.php
$productRepository = $entityManager->getRepository('Product');
$products = $productRepository->findAll();
foreach ($products as $product) {
echo sprintf("-%s\n", $product->getName());
}
The ``EntityRepository`` fetched through the ``EntityManager#getRepository()``
method exists for every entity and is provided by Doctrine. It contains
some finder methods such as ``findAll()`` we used here.
Lets display the name of a product based on its ID:
.. code-block:: php
<?php
// show_product.php
$id = $argv[1];
$product = $entityManager->find('Product', $id);
echo sprintf("-%s\n", $product->getName());
Adding Bug and User Entities
----------------------------
We continue with the bug tracker domain, by creating the missing
classes ``Bug`` and ``User`` and putting them into
`src/Bug.php` and `src/User.php`
respectively.
.. code-block:: php
<?php
// src/Bug.php
class Bug
{
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $description;
/**
* @var DateTime
*/
protected $created;
/**
* @var string
*/
protected $status;
}
.. code-block:: php
<?php
@ -199,25 +478,6 @@ entity. Entity classes are part of the domain model of your application.
}
}
.. note::
**What are Entities?**
Entities are PHP Objects that can be identified over many requests
by a unique identifier or primary key. These classes don't need to extend any
abstract base class or interface. An entity class must not be final
or contain final methods. Additionally it must not implement
**clone** nor **wakeup** or :doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
An entity contains persistable properties. A persistable property
is an instance variable of the entity that is saved into and retrieved from the database
by Doctrine's data mapping capabilities.
Note how all properties have getter and setter methods defined except
`$id`. To access data from entities Doctrine 2 uses the Reflection API, so it
is possible for Doctrine to access the value of `$id`. You don't have to
take Doctrine into account when designing access to the state of your objects.
All of the properties so far are scalar values, for example the 3 ID
fields of the entities, their names, description, status and change dates.
@ -296,7 +556,6 @@ persistence.
Debug::dump() method just ignores any occurrences of it in Proxy
instances.
Because we only work with collections for the references we must be
careful to implement a bidirectional reference in the domain model.
The concept of owning or inverse side of a relation is central to
@ -448,112 +707,8 @@ the database that points from Bugs to Products.
}
We are now finished with the domain model given the requirements.
From the simple model with public properties only we had to do
quite some work to get to a model where we encapsulated the
references between the objects to make sure we don't break its
consistent state when using Doctrine.
However up to now the assumptions Doctrine imposed on our business
objects have not restricting us much in our domain modelling
capabilities. Actually we would have encapsulated access to all the
properties anyways by using object-oriented best-practices.
Metadata Mappings for our Entities
----------------------------------
Up to now we have only implemented our Entities as Data-Structures
without actually telling Doctrine how to persist them in the
database. If perfect in-memory databases would exist, we could now
finish the application using these entities by implementing code to
fulfil all the requirements. However the world isn't perfect and we
have to persist our entities in some storage to make sure we don't
loose their state. Doctrine currently serves Relational Database
Management Systems. In the future we are thinking to support NoSQL
vendors like CouchDb or MongoDb, however this is still far in the
future.
The next step for persistence with Doctrine is to describe the
structure of our domain model entities to Doctrine using a metadata
language. The metadata language describes how entities, their
properties and references should be persisted and what constraints
should be applied to them.
Metadata for entities are loaded using a
``Doctrine\ORM\Mapping\Driver\Driver`` implementation and Doctrine
2 already comes with XML, YAML and Annotations Drivers. This
Getting Started Guide will show the mappings for all Mapping Drivers.
References in the text will be made to the XML mapping.
Since we haven't namespaced our three entities, we have to
implement three mapping files called Bug.dcm.xml, Product.dcm.xml
and User.dcm.xml (or .yml) and put them into a distinct folder for mapping
configurations. For the annotations driver we need to use
doc-block comments on the entity classes themselves.
The first discussed definition will be for the Product, since it is
the most simple one:
.. configuration-block::
.. code-block:: php
<?php
// src/Product.php
/**
* @Entity @Table(name="products")
**/
class Product
{
/** @Id @Column(type="integer") @GeneratedValue **/
protected $id;
/** @Column(type="string") **/
protected $name;
// .. (other code)
}
.. code-block:: xml
<!-- config/xml/Product.dcm.xml -->
<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://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
<entity name="Product" table="products">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="name" type="string" />
</entity>
</doctrine-mapping>
.. code-block:: yaml
# config/yaml/Product.dcm.yml
Product:
type: entity
table: products
id:
id:
type: integer
generator:
strategy: AUTO
fields:
name:
type: string
The top-level ``entity`` definition tag specifies information about
the class and table-name. The primitive type ``Product::$name`` is
defined as ``field`` attributes. The Id property is defined with
the ``id`` tag. The id has a ``generator`` tag nested inside which
defines that the primary key generation mechanism automatically
uses the database platforms native id generation strategy, for
example AUTO INCREMENT in the case of MySql or Sequences in the
case of PostgreSql and Oracle.
We then go on specifying the definition of a Bug:
Now we continue adding metadata mappings for the ``User`` and ``Bug``
as we did for the ``Product`` before:
.. configuration-block::
.. code-block:: php
@ -654,7 +809,7 @@ We then go on specifying the definition of a Bug:
targetEntity: Product
Here again we have the entity, id and primitive type definitions.
Here we have the entity, id and primitive type definitions.
The column names are used from the Zend\_Db\_Table examples and
have different names than the properties on the Bug class.
Additionally for the "created" field it is specified that it is of
@ -774,118 +929,10 @@ class that holds the owning sides.
This example has a fair overview of the most basic features of the
metadata definition language.
Obtaining the EntityManager
---------------------------
Doctrine's public interface is the EntityManager, it provides the
access point to the complete lifecycle management of your entities
and transforms entities from and back to persistence. You have to
configure and create it to use your entities with Doctrine 2. I
will show the configuration steps and then discuss them step by
step:
.. code-block:: php
<?php
// bootstrap.php
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
require_once "vendor/autoload.php";
// Create a simple "default" Doctrine ORM configuration for Annotations
$isDevMode = true;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/entities"), $isDevMode);
// or if you prefer yaml or XML
//$config = Setup::createXMLMetadataConfiguration(array(__DIR__."/config/xml"), $isDevMode);
//$config = Setup::createYAMLMetadataConfiguration(array(__DIR__."/config/yaml"), $isDevMode);
// database configuration parameters
$conn = array(
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/db.sqlite',
);
// obtaining the entity manager
$entityManager = EntityManager::create($conn, $config);
The first require statement sets up the autoloading capabilities of Doctrine
using the Composer autoload.
The second block consists of the instantiation of the ORM
``Configuration`` object using the Setup helper. It assumes a bunch
of defaults that you don't have to bother about for now. You can
read up on the configuration details in the
:doc:`reference chapter on configuration <../reference/configuration>`.
The third block shows the configuration options required to connect
to a database, in my case a file-based sqlite database. All the
configuration options for all the shipped drivers are given in the
`DBAL Configuration section of the manual <http://www.doctrine-project.org/documentation/manual/2_0/en/dbal>`_.
The last block shows how the ``EntityManager`` is obtained from a
factory method.
Generating the Database Schema
Implementing more Requirements
------------------------------
Now that we have defined the Metadata Mappings and bootstrapped the
EntityManager we want to generate the relational database schema
from it. Doctrine has a Command-Line-Interface that allows you to
access the SchemaTool, a component that generates the required
tables to work with the metadata.
For the command-line tool to work a cli-config.php file has to be
present in the project root directory, where you will execute the
doctrine command. Its a fairly simple file:
.. code-block:: php
<?php
// cli-config.php
require_once "bootstrap.php";
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($entityManager)
));
You can then change into your project directory and call the
Doctrine command-line tool:
::
$ cd project/
$ php vendor/bin/doctrine orm:schema-tool:create
During the development you probably need to re-create the database
several times when changing the Entity metadata. You can then
either re-create the database:
::
$ php vendor/bin/doctrine orm:schema-tool:drop --force
$ php vendor/bin/doctrine orm:schema-tool:create
Or use the update functionality:
::
$ php vendor/bin/doctrine orm:schema-tool:update --force
The updating of databases uses a Diff Algorithm for a given
Database Schema, a cornerstone of the ``Doctrine\DBAL`` package,
which can even be used without the Doctrine ORM package. However
its not available in SQLite since it does not support ALTER TABLE.
Writing Entities into the Database
----------------------------------
.. note::
This tutorial assumes you call all the example scripts from the CLI.
Having created the schema we can now start and save entities in the
database. For starters we need a create user use-case:
For starters we need a create user entities:
.. code-block:: php
@ -903,59 +950,14 @@ database. For starters we need a create user use-case:
echo "Created User with ID " . $user->getId() . "\n";
Products can also be created:
.. code-block:: php
<?php
// create_product.php
require_once "bootstrap.php";
$newProductName = $argv[1];
$product = new Product();
$product->setName($newProductName);
$entityManager->persist($product);
$entityManager->flush();
echo "Created Product with ID " . $product->getId() . "\n";
Now call:
::
$ php create_user.php beberlei
$ php create_product.php MyProduct
So what is happening in those two snippets? In both examples the
code that works on User and Product is pretty standard OOP. The interesting bits are the
communication with the ``EntityManager``. To notify the
EntityManager that a new entity should be inserted into the
database you have to call ``persist()``. However the EntityManager
does not act on this command, its merely notified. You have to explicitly
call ``flush()`` to have the EntityManager write those two entities
to the database.
You might wonder why does this distinction between persist
notification and flush exist: Doctrine 2 uses the UnitOfWork
pattern to aggregate all writes (INSERT, UPDATE, DELETE) into one
single transaction, which is executed when flush is called.
Using this approach the write-performance is significantly better
than in a scenario where updates are done for each entity in
isolation. In more complex scenarios than the previous two, you are
free to request updates on many different entities and all flush
them at once.
Doctrine's UnitOfWork detects entities that have changed after
retrieval from the database automatically when the flush operation
is called, so that you only have to keep track of those entities
that are new or to be removed and pass them to
``EntityManager#persist()`` and ``EntityManager#remove()``
respectively.
We are now getting to the "Create a New Bug" requirement and the
code for this scenario may look like this:
We now have the data to create a bug and the code for this scenario may look
like this:
.. code-block:: php
@ -1462,4 +1464,3 @@ will be added to this tutorial incrementally, topics will include:
Additional details on all the topics discussed here can be found in
the respective manual chapters.