Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editing the Doctrine section to improve accuracy and readability #6455

Closed
wants to merge 7 commits into from
251 changes: 136 additions & 115 deletions book/doctrine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -721,31 +721,31 @@ Doctrine Query Language (DQL). DQL is similar to SQL except that you should
imagine that you're querying for one or more objects of an entity class (e.g. ``Product``)
instead of querying for rows on a table (e.g. ``product``).

When querying in Doctrine, you have two options: writing pure Doctrine queries
When querying in Doctrine, you have two main options: writing pure DQL queries
or using Doctrine's Query Builder.

Querying for Objects with DQL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Imagine that you want to query for products, but only return products that
cost more than ``19.99``, ordered from cheapest to most expensive. You can use
Doctrine's native SQL-like language called DQL to make a query for this::
Imagine that you want to query for products that cost more than ``19.99``,
ordered from least to most expensive. You can use DQL, Doctrine's native
SQL-like language, to construct a query for this scenario::

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT p
FROM AppBundle:Product p
WHERE p.price > :price
ORDER BY p.price ASC'
)->setParameter('price', '19.99');
)->setParameter('price', 19.99);

$products = $query->getResult();

If you're comfortable with SQL, then DQL should feel very natural. The biggest
difference is that you need to think in terms of "objects" instead of rows
in a database. For this reason, you select *from* the ``AppBundle:Product``
*object* (an optional shortcut for ``AppBundle\Entity\Product``) and then
alias it as ``p``.
difference is that you need to think in terms of selecting PHP objects,
instead of rows in a database. For this reason, you select *from* the
``AppBundle:Product`` *entity* (an optional shortcut for the
``AppBundle\Entity\Product`` class) and then alias it as ``p``.

.. tip::

Expand Down Expand Up @@ -799,11 +799,11 @@ Custom Repository Classes
~~~~~~~~~~~~~~~~~~~~~~~~~

In the previous sections, you began constructing and using more complex queries
from inside a controller. In order to isolate, test and reuse these queries,
it's a good practice to create a custom repository class for your entity and
add methods with your query logic there.
from inside a controller. In order to isolate, reuse and test these queries,
it's a good practice to create a custom repository class for your entity.
Methods containing your query logic can then be stored in this class.

To do this, add the name of the repository class to your mapping definition:
To do this, add the repository class name to your entity's mapping definition:

.. configuration-block::

Expand Down Expand Up @@ -847,16 +847,22 @@ To do this, add the name of the repository class to your mapping definition:
</entity>
</doctrine-mapping>

Doctrine can generate the repository class for you by running the same command
used earlier to generate the missing getter and setter methods:
Doctrine can generate empty repository classes for all the entities in your
application via the same command used earlier to generate the missing getter
and setter methods:

.. code-block:: bash

$ php app/console doctrine:generate:entities AppBundle

Next, add a new method - ``findAllOrderedByName()`` - to the newly generated
repository class. This method will query for all the ``Product`` entities,
ordered alphabetically.
.. tip::

If you opt to create the repository classes yourself, they must extend
``Doctrine\ORM\EntityRepository``.

Next, add a new method - ``findAllOrderedByName()`` - to the newly-generated
product repository class. This method will query for all the ``Product``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably say ProductRepository class

entities, ordered alphabetically by name.

.. code-block:: php

Expand Down Expand Up @@ -898,11 +904,13 @@ You can use this new method just like the default finder methods of the reposito
Entity Relationships/Associations
---------------------------------

Suppose that the products in your application all belong to exactly one "category".
In this case, you'll need a ``Category`` object and a way to relate a ``Product``
object to a ``Category`` object. Start by creating the ``Category`` entity.
Since you know that you'll eventually need to persist the class through Doctrine,
you can let Doctrine create the class for you.
Suppose that each product in your application belongs to exactly one category.
In this case, you'll need a ``Category`` class, and a way to relate a
``Product`` object to a ``Category`` object.

Start by creating the ``Category`` entity. Since you know that you'll eventually
need to persist category objects through Doctrine, you can let Doctrine create
the class for you.

.. code-block:: bash

Expand All @@ -916,8 +924,81 @@ a ``name`` field and the associated getter and setter functions.
Relationship Mapping Metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt like a more-detailed explanation of the difference between OneToMany and ManyToOne was warranted here, since we're dealing with readers who may not be familiar with those terms (and who may be confused by the fact that both terms can be used to describe the same relationship).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like that you switched the order to show ManyToOne first. I wrote this original spot, but it's bothered me for a long time that we showed OneToMany first (I hadn't had a chance to change it).

To relate the ``Category`` and ``Product`` entities, start by creating a
``products`` property on the ``Category`` class:
In our example, each category can be associated with *many* products, while
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last comment: In the docs we never used the first person perspective. So I would change this to "In this example, [...]".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thanks for the thorough review on this PR!

each product can be associated with only *one* category. This relationship
can be summarized as: *many* products to *one* category (or equivalently,
*one* category to *many* products).

From the perspective of the ``Product`` entity, this is a many-to-one relationship.
From the perspective of the ``Category`` entity, this is a one-to-many relationship.
This is important, because the relative nature of the relationship determines
which mapping metadata to use. It also determines which class *must* hold
a reference to the other class.

To relate the ``Product`` and ``Category`` entities, simply create a ``category``
property on the ``Product`` class, annotated as follows:

.. configuration-block::

.. code-block:: php-annotations

// src/AppBundle/Entity/Product.php

// ...
class Product
{
// ...

/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove the @ORM\JoinColumn annotation. It's optional and, at this point, it complicates everything for no clear benefit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the annotation is admittedly optional in this situation, I think it should be maintained because it makes the code more expressive. To a beginner who might have trouble imagining which of the two entities actually holds the foreign key, the @ORM\JoinColumn quickly answers that question (since it exists only in Product, but not Category).

The presence of this annotation also facilitates an easy discussion of the category_id column further down in the tutorial. In the absence of @ORM\JoinColumn, we'd have to then veer into an explanation of how Doctrine automatically determines the name of the foreign key field, and how Doctrine knows in which of the two tables the foreign key should be stored.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. It's clear to me that you are right :) Thanks for the explanation!

*/
private $category;
}

.. code-block:: yaml

# src/AppBundle/Resources/config/doctrine/Product.orm.yml
AppBundle\Entity\Product:
type: entity
# ...
manyToOne:
category:
targetEntity: Category
inversedBy: products
joinColumn:
name: category_id
referencedColumnName: id

.. code-block:: xml

<!-- src/AppBundle/Resources/config/doctrine/Product.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="AppBundle\Entity\Product">
<!-- ... -->
<many-to-one
field="category"
target-entity="Category"
inversed-by="products"
join-column="category">

<join-column name="category_id" referenced-column-name="id" />
</many-to-one>
</entity>
</doctrine-mapping>

This mapping is critical, as it tells Doctrine to use the ``category_id``
column on the ``products`` table to relate each record in that table with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

product table

a record in the ``category`` table.

Next, since a single ``Category`` object will relate to many ``Product``
objects, a ``products`` property can be added to the ``Category`` class
to hold those associated objects.

.. configuration-block::

Expand Down Expand Up @@ -979,126 +1060,66 @@ To relate the ``Category`` and ``Product`` entities, start by creating a
</entity>
</doctrine-mapping>

First, since a ``Category`` object will relate to many ``Product`` objects,
a ``products`` array property is added to hold those ``Product`` objects.
Again, this isn't done because Doctrine needs it, but instead because it
makes sense in the application for each ``Category`` to hold an array of
``Product`` objects.
Doctrine does not *require* that the "one" side of a one-to-many relationship
hold a collection of its "many" related entities. But in the context of our
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to split hairs, but using the present subjunctive is grammatically appropriate here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? The verb refers to the "the one side" subject of this sentence, doesn't it? And this is in its singular form.

We could also change it to "Doctrine does not require the "one" side of a one-to-many relationship to hold [...]". What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the subject here is "the one side" (3rd person, singular). However, the 3rd person singular conjugation of the verb "hold" in the present subjunctive is hold, not holds. http://www.verbix.com/webverbix/English/hold.html But since this has caused some unnecessary confusion, I'll change the phrasing per your second suggestion.

application, it makes sense for each ``Category`` object to hold an
array of ``Product`` objects. However, if we had decided against adding
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it's not an array. I think we should use another term here (e.g. "collection") to not confuse the reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point & suggestion!

a ``$products`` property to the ``Category`` class, then the ``Product``
entity's ``inversedBy`` metadata would not have been necessary.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] would have to be omitted.


.. note::

The code in the ``__construct()`` method is important because Doctrine
requires the ``$products`` property to be an ``ArrayCollection`` object.
The code in the constructor is important because the ``$products`` property
must be an ``ArrayCollection`` object, rather than a traditional ``array``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not important that it is an ArrayCollection, but it must be a Collection implementation (for example, Doctrine will populate the property with a PersistentCollection instance by default when data are queried from the database).

This object looks and acts almost *exactly* like an array, but has some
added flexibility. If this makes you uncomfortable, don't worry. Just
imagine that it's an ``array`` and you'll be in good shape.

.. tip::

The targetEntity value in the decorator used above can reference any entity
The targetEntity value in the metadata used above can reference any entity
with a valid namespace, not just entities defined in the same namespace. To
relate to an entity defined in a different class or bundle, enter a full
namespace as the targetEntity.

Next, since each ``Product`` class can relate to exactly one ``Category``
object, you'll want to add a ``$category`` property to the ``Product`` class:

.. configuration-block::

.. code-block:: php-annotations

// src/AppBundle/Entity/Product.php

// ...
class Product
{
// ...

/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}

.. code-block:: yaml

# src/AppBundle/Resources/config/doctrine/Product.orm.yml
AppBundle\Entity\Product:
type: entity
# ...
manyToOne:
category:
targetEntity: Category
inversedBy: products
joinColumn:
name: category_id
referencedColumnName: id

.. code-block:: xml

<!-- src/AppBundle/Resources/config/doctrine/Product.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="AppBundle\Entity\Product">
<!-- ... -->
<many-to-one
field="category"
target-entity="Category"
inversed-by="products"
join-column="category">

<join-column name="category_id" referenced-column-name="id" />
</many-to-one>
</entity>
</doctrine-mapping>

Finally, now that you've added a new property to both the ``Category`` and
``Product`` classes, tell Doctrine to generate the missing getter and setter
methods for you:
Now that you've added new properties to both the ``Product`` and ``Category``
classes, tell Doctrine to generate the missing getter and setter methods for you:

.. code-block:: bash

$ php app/console doctrine:generate:entities AppBundle

Ignore the Doctrine metadata for a moment. You now have two classes - ``Category``
and ``Product`` with a natural one-to-many relationship. The ``Category``
class holds an array of ``Product`` objects and the ``Product`` object can
hold one ``Category`` object. In other words - you've built your classes
in a way that makes sense for your needs. The fact that the data needs to
be persisted to a database is always secondary.

Now, look at the metadata above the ``$category`` property on the ``Product``
class. The information here tells Doctrine that the related class is ``Category``
and that it should store the ``id`` of the category record on a ``category_id``
field that lives on the ``product`` table. In other words, the related ``Category``
object will be stored on the ``$category`` property, but behind the scenes,
Doctrine will persist this relationship by storing the category's id value
on a ``category_id`` column of the ``product`` table.
Ignore the Doctrine metadata for a moment. You now have two classes - ``Product``
and ``Category``, with a natural many-to-one relationship. The ``Product``
class holds a *single* ``Category`` object, and the ``Category`` class holds
a *collection* of ``Product`` objects. In other words, you've built your classes
in a way that makes sense for your application. The fact that the data needs
to be persisted to a database is always secondary.

Now, review the metadata that was added above the ``Product`` entity's
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"was added" made me think that the doctrine:generate:entities command added some additional metadata. How about simple: review the metadata above the...

``$category`` property. It tells Doctrine that the related class is ``Category``,
and that the ``id`` of the related category record should be stored in a
``category_id`` field on the ``product`` table.

In other words, the related ``Category`` object will be stored in the
``$category`` property, but behind the scenes, Doctrine will persist this
relationship by storing the category's id in the ``category_id`` column
of the ``product`` table.

.. image:: /images/book/doctrine_image_2.png
:align: center

The metadata above the ``$products`` property of the ``Category`` object
is less important, and simply tells Doctrine to look at the ``Product.category``
The metadata above the ``Category`` entity's ``$products`` property is less
complicated. It simply tells Doctrine to look at the ``Product.category``
property to figure out how the relationship is mapped.

Before you continue, be sure to tell Doctrine to add the new ``category``
table, and ``product.category_id`` column, and new foreign key:
table, the new ``product.category_id`` column, and the new foreign key:

.. code-block:: bash

$ php app/console doctrine:schema:update --force

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this warning, since we previously discussed the usage of doctrine:schema:update --force earlier in this chapter.

.. note::

This command should only be used during development. For a more robust
method of systematically updating your production database, read about
`migrations`_.

Saving Related Entities
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down