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

Already on GitHub? Sign in to your account

The new Doctrine chapter has landed #366

Merged
merged 20 commits into from Jun 5, 2011

Conversation

Projects
None yet
8 participants
Member

weaverryan commented Jun 4, 2011

Hey guys!

This is a complete refactor of the Doctrine chapter. This is ready - except for 3 missing diagrams - which @Leannapelham and should knock out this weekend. The only known issue is that the doctrine:generate:entity command does not currently work, but I doc'ed it here as if it did (i.e. hopefully it will soon).

This is too big of a chapter for me to have gotten everything right, so comments+proofreading much appreciated.

Thanks!

@lmcd lmcd commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+ Next, be sure that the new bundle is enabled in the kernel::
+
+ // app/AppKernel.php
+
+ public function registerBundles()
+ {
+ $bundles = array(
+ // ...
+ new Acme\StoreBundle\AcmeStoreBundle(),
+ );
+ }
+
+Configuring the Database
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before you really being, you'll need to configure your database connection
@lmcd

lmcd Jun 4, 2011

Contributor

Typo: being = begin?

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+
+A Simple Example: A Product
+---------------------------
+
+The easiest way to understand how Doctrine works is to see it in action.
+In this section, you'll configure your database, create a ``Product`` object,
+persist it to the database and fetch it back out.
+
+.. sidebar:: Code along with the example
+
+ If you want to follow along with the example in this chapter, create
+ an ``AcmeStoreBundle`` via:
+
+ .. code-block:: bash
+
+ php app/console init:bundle "Acme\StoreBundle" src/
@fabpot

fabpot Jun 4, 2011

Owner

What about using / on the CLI instead of \? It works the same and there is no need to remember to use " in this case.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ Defining the configuration via ``parameters.ini`` is just a convention.
+ The parameters defined in that file are referenced by the main configuration
+ file when setting up Doctrine:
+
+ .. code-block:: yaml
+
+ doctrine:
+ dbal:
+ driver: %database_driver%
+ host: %database_host%
+ dbname: %database_name%
+ user: %database_user%
+ password: %database_password%
+
+ By separating the database information into a separate file, you can
+ easily keep different version of the file on each server.
@fabpot

fabpot Jun 4, 2011

Owner

You can also add a link to http://symfony.com/doc/current/cookbook/configuration/external_parameters.html where we explain how to set sensitive information outside your project.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ dbal:
+ driver: %database_driver%
+ host: %database_host%
+ dbname: %database_name%
+ user: %database_user%
+ password: %database_password%
+
+ By separating the database information into a separate file, you can
+ easily keep different version of the file on each server.
+
+Now that Doctrine knows about your database, you can have it create the database
+for you:
+
+.. code-block:: bash
+
+ php app/console doctrine:database:create
@fabpot

fabpot Jun 4, 2011

Owner

IIRC, it does not always work (for SQLite for instance). I tend to prefer using the native tool for this step.

@weaverryan

weaverryan Jun 4, 2011

Member

Hmm, it does work for SQLite (just tried that one specifically). If this isn't dependable, I want to remove it. If it is, I'd like to keep it - it's nice, and the user can work inside symfony exclusively.

@beberlei

beberlei Jun 4, 2011

Contributor

It wont work for Oracle and only in certain cases on Postgres, but i guess its still good to mention.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ protected $description;
+ }
+
+The class - often called an "entity", meaning *a basic class that holds data*
+- is simple and helps fulfill the business requirement of needing products
+in your application. This class can't be persisted to a database yet - it's
+just a simple PHP class.
+
+.. tip::
+
+ Once you learn the concepts behind Doctrine, you can have Doctrine create
+ this entity class for you:
+
+ .. code-block:: bash
+
+ php app/console doctrine:generate:entity AcmeStoreBundle:Product
@fabpot

fabpot Jun 4, 2011

Owner

So that this command is more or less equivalent to the example above, you should probably use:

php app/console doctrine:generate:entity AcmeStoreBundle:Product "name:string(255) price:float description:text"

@fabpot fabpot commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+.. tip::
+
+ The table option is optional and if omitted, will be determined automatically
+ based on the name of the entity class.
+
+Doctrine allows you to choose from a wide variety of different field types,
+each with their own options. For information on the available field types,
+see the :ref:`book-doctrine-field-types` section.
+
+.. seealso::
+
+ You can also check out Doctrine's `Basic Mapping Documentation`_ for
+ all details about mapping information. Keep in mind that when you use
+ Doctrine inside Symfony, you'll need to prepend all annotations (if that's
+ your preferred format) with ``ORM\`` (e.g. ``ORM\Column(..)``), which
+ is not shown in Doctrine's documentation.
@fabpot

fabpot Jun 4, 2011

Owner

Not sure if you should should also note to not forget the use statement.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ Doctrine inside Symfony, you'll need to prepend all annotations (if that's
+ your preferred format) with ``ORM\`` (e.g. ``ORM\Column(..)``), which
+ is not shown in Doctrine's documentation.
+
+Generating Getters and Setters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Even though Doctrine now knows how to persist a ``Product`` object to the
+database, the class itself isn't really useful yet. Since ``Product`` is just
+a regular PHP class, you need to create getter and setter methods (e.g. ``getName()``,
+``setName()``) in order to access its properties (since the properties are
+``protected``). Fortunately, Doctrine can do this for you by running:
+
+.. code-block:: bash
+
+ php app/console doctrine:generate:entities Acme
@fabpot

fabpot Jun 4, 2011

Owner

What about using:

php app/console doctrine:generate:entities AcmeStoreBundle:Product

And add a tip about how to generate entities for a given namespace or for a given bundle.

@fabpot

fabpot Jun 4, 2011

Owner

As you introduce the shortcut notation later (https://github.com/symfony/symfony-docs/pull/366/files#L0R392), you can use the FQCN:

php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product

@fabpot fabpot commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+ :linenos:
+
+ // src/Acme/StoreBundle/Controller/DefaultController.php
+
+ public function createAction()
+ {
+ $product = new Product();
+ $product->setName('A Foo Bar');
+ $product->setPrice('19.99');
+ $product->setDescription('Lorem ipsum dolor');
+
+ $em = $this->get('doctrine')->getEntityManager();
+ $em->persist($product);
+ $em->flush();
+
+ return new Symfony\Component\HttpFoundation\Response(
@fabpot

fabpot Jun 4, 2011

Owner

Can you use a use statement instead here? That's the convention we use everywhere else (and it makes the code more readable.)

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ $product->setName('A Foo Bar');
+ $product->setPrice('19.99');
+ $product->setDescription('Lorem ipsum dolor');
+
+ $em = $this->get('doctrine')->getEntityManager();
+ $em->persist($product);
+ $em->flush();
+
+ return new Symfony\Component\HttpFoundation\Response(
+ 'Created product id '.$product->getId()
+ );
+ }
+
+.. note::
+
+ If you're following along with this example, you'll need to create a
@fabpot

fabpot Jun 4, 2011

Owner

What not use a Route annotation?

@weaverryan

weaverryan Jun 4, 2011

Member

Went that route (no pun intended) initially, but I also then need to tell them to import the routes from their controller. It started to get a little long-winded. What do you think?

@fabpot

fabpot Jun 4, 2011

Owner

You're probably right.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+
+* **lines 5-8** In this section, you instantiate and work with the ``$product``
+ object like any other, normal PHP object;
+
+* **line 10** This line fetches Doctrine's *entity manager* object, which is
+ responsibly for handling the process of persisting and fetching objects
+ to and from the database;
+
+* **line 11** The ``persist()`` method tells Doctrine to "manage" the ``$product``
+ object. This does not actually cause a query to be made to the database (yet).
+
+* **line 12** When the ``flush()`` method is called, Doctrine looks through
+ all of the objects that it's managing to see if they need to be persisted
+ to the database. In this example, the ``$product`` object has not been
+ persisted yet, so the entity manager executes an ``INSERT`` query and a
+ row is created in the ``product`` table.
@fabpot

fabpot Jun 4, 2011

Owner

Do we need to introduce why this step is a good idea? Perhaps a word or two about the unit of work and its benefits?

@fabpot fabpot commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+* **line 11** The ``persist()`` method tells Doctrine to "manage" the ``$product``
+ object. This does not actually cause a query to be made to the database (yet).
+
+* **line 12** When the ``flush()`` method is called, Doctrine looks through
+ all of the objects that it's managing to see if they need to be persisted
+ to the database. In this example, the ``$product`` object has not been
+ persisted yet, so the entity manager executes an ``INSERT`` query and a
+ row is created in the ``product`` table.
+
+When creating or updating objects, the workflow is always the same. In the
+next section, you'll see how Doctrine is smart enough to automatically issue
+an ``UPDATE`` query if the record already exists in the database.
+
+.. tip::
+
+ Symfony provides a bundle that allows you to programmatically load testing
@fabpot

fabpot Jun 4, 2011

Owner

"Doctrine provides a library that allows..."

@fabpot fabpot commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+Once you've fetched an object from Doctrine, updating it is easy. Suppose
+you have a route that maps a product id to an update action in a controller::
+
+ public function updateAction($id)
+ {
+ $em = $this->get('doctrine')->getEntityManager();
+ $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);
+
+ if (!$product) {
+ throw $this->createNotFoundException('No product found for id '.$id);
+ }
+
+ $product->setName('New product name!');
+ $em->flush();
+
+ return $this->redirect($this->generate('homepage'));
@fabpot

fabpot Jun 4, 2011

Owner

This is generateUrl instead of generate.

@fabpot fabpot commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+====================================
+
+Let's face it, one of the most common and challenging tasks for any application
+involves persisting and reading information to and from a database. Fortunately,
+Symfony comes integrated with `Doctrine`_, a library whose sole goal is to
+give you powerful tools to make this easy. In this chapter, you'll learn the
+basic philosophy behind Doctrine and see how easy working with a database can
+be.
+
+.. note::
+
+ This chapter is all about the Doctrine ORM, which means persistence to
+ relational databases such as *MySQL*, *PostgreSQL* or *Microsoft SQL*.
+ A Doctrine ODM library also exists, which persist data to `MongoDB`_.
+ For more information, read the ":doc:`/cookbook/doctrine/mongodb`" cookbook entry.
+
@fabpot

fabpot Jun 4, 2011

Owner

I think we need to make it clear that using Doctrine is totally optional and totally decoupled from Symfony. Also, we can just hint about using raw DB queries with Doctrine DBAL (perhaps with a link to an "upcoming" recipe there).

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ ->getEntityManager()
+ ->getRepository('AcmeStoreBundle:Product');
+
+ $query = $repository->createQueryBuilder('p')
+ ->where('p.price > :price')
+ ->setParameter('price', '19.99')
+ ->orderBy('p.price', 'ASC')
+ ->getQuery();
+
+ $products = $query->getResult();
+
+The ``QueryBuilder`` object contains every method necessary to build your
+query. By calling the ``getQuery()`` method, the query builder returns a
+normal ``Query`` object, which is the same object you built directly in the
+previous section.
+
@fabpot

fabpot Jun 4, 2011

Owner

which has the added benefit of providing auto-completion.

@fabpot fabpot and 4 others commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+
+ // src/Acme/StoreBundle/Entity/Category.php
+ // ...
+
+ class Category
+ {
+ // ...
+
+ /**
+ * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
+ */
+ protected $products;
+
+ public function __construct()
+ {
+ $this->products = new Doctrine\Common\Collections\ArrayCollection();
@fabpot

fabpot Jun 4, 2011

Owner

Can you move the namespace to a use statement?

@weaverryan

weaverryan Jun 4, 2011

Member

I'm moving this, but I do get concerned sometimes that the user won't see the new use statement, which is quite a few lines above the "meat" of the example. What's worse is that the error when you don't include the use statement is a bit obtuse: "Class Acme\StoreBundle\Entity\ArrayCollection not found", but of course, I can't imagine any way we could really help that out.

@schmittjoh

schmittjoh Jun 4, 2011

Contributor

These kinds of errors are hard to address in a general way. In the DebuggingBundle, I have started to add a few "ProblemSolvers" for specific cases, see for example https://github.com/schmittjoh/DebuggingBundle/commit/45e61e1907bfcd2309ae7514067612e869be7047

I'm not sure if that is possible in this specific case, but in principle it seems like a good approach to me.

@breerly

breerly Jun 6, 2011

Contributor

really happy to see a OneToMany make it to the Symfony2 docs. Doctrine's own guide is ... difficult.

@beberlei

beberlei Jun 6, 2011

Contributor

You can easily edit the doctrine docs on the web or just open a ticket with this complaint, without user-feedback we cannot change anything for the better.

@fabpot fabpot commented on the diff Jun 4, 2011

book/doctrine.rst
+ {
+ $category = $this->get('doctrine')
+ ->getEntityManager()
+ ->getRepository('AcmeStoreBundle:Category')
+ ->find($id);
+
+ $products = $category->getProducts();
+
+ // ...
+ }
+
+In this case, the same things occurs: you first query out for a single ``Category``
+object, and then Doctrine makes a second query to retrieve the related ``Product``
+objects, but only once/if you ask for them (i.e. when you call ``->getProducts()``).
+The ``$products`` variable is an array of all ``Product`` objects that relate
+to the given ``Category`` object via their ``category_id`` value.
@fabpot

fabpot Jun 4, 2011

Owner

What about adding a note about how this "magic" works and the proxy classes?

Member

weaverryan commented Jun 4, 2011

Thanks :) - all of these changes have been integrated - I'll merge as soon as the diagrams are in there.

@beberlei beberlei commented on an outdated diff Jun 4, 2011

book/doctrine.rst
+* **line 11** The ``persist()`` method tells Doctrine to "manage" the ``$product``
+ object. This does not actually cause a query to be made to the database (yet).
+
+* **line 12** When the ``flush()`` method is called, Doctrine looks through
+ all of the objects that it's managing to see if they need to be persisted
+ to the database. In this example, the ``$product`` object has not been
+ persisted yet, so the entity manager executes an ``INSERT`` query and a
+ row is created in the ``product`` table.
+
+.. note::
+
+ In fact, since Doctrine is aware of all your managed entities, when you
+ call the ``flush()`` method, it calculates an overall changeset and executes
+ the most efficient query/queries possible. For example, if you're persist
+ 100 ``Product`` objects and then call ``persist()``, Doctrine will execute
+ a *single*, multi-line ``INSERT`` query (assuming your database engine supports
@beberlei

beberlei Jun 4, 2011

Contributor

This is wrong, it will just create a single prepared statement and re-use that. Multi line insert is not supported across all vendors.

@beberlei

beberlei Jun 4, 2011

Contributor

And all databases support prepared statements (or PDO emulates that for them, but that is not something important for users to know)

@beberlei beberlei commented on the diff Jun 4, 2011

book/doctrine.rst
+Fetching Objects from the Database
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Fetching an object back out of the database is even easier. For example,
+suppose you've configured a route to display a specific ``Product`` based
+on its ``id`` value::
+
+ public function showAction($id)
+ {
+ $product = $this->get('doctrine')
+ ->getEntityManager()
+ ->getRepository('AcmeStoreBundle:Product')
+ ->find($id);
+
+ if (!$product) {
+ throw $this->createNotFoundException('No product found for id '.$id);
@beberlei

beberlei Jun 4, 2011

Contributor

Not relevant to doctrine, but:

This looks weird, most people are used to just throw new foo. Why not $this->throwNotFoundException(...); ?

@weaverryan

weaverryan Jun 4, 2011

Member

This was discussed here: symfony/symfony#394

My main goal was to prevent the user from needing to use the exception class. This accomplishes that, but doesn't necessarily mean that it's the best execution. Fabien brought up the point of not wanting to hide the fact that an exception is thrown, though your proposed method pretty clearly describes that an exception is being thrown.

Thoughts?

Doctrine is providing this library, not Symfony.

Will you merge the current dbal cookbook entry with this new one?

Member

weaverryan replied Jun 4, 2011

already forgot about that entry - this one has been deleted and the link changed to the existing entry

@stof stof commented on an outdated diff Jun 5, 2011

book/doctrine.rst
+ php app/console doctrine:generate:entity AcmeStoreBundle:Product "name:string(255) price:float description:text"
+
+2) Add Mapping Information
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Doctrine allows you to work with databases in a much more interesting way
+than just fetching rows of column-based table into an array. Instead, Doctrine
+allows you to persist entire *objects* to the database and fetch entire objects
+out of the database. This works by mapping a PHP class to a database table,
+and the properties of that PHP class to columns on the table:
+
+ DIAGRAM here of the Product class on the left (looking like an object
+ with visible name, price, description properties) and a "product" table
+ on the right, with name, price and description columns. In the middle
+ is Doctrine, which is handling a two-way street, transforming data in
+ both directions.
@stof

stof Jun 5, 2011

Member

The diagram is still missing.

@stof stof commented on the diff Jun 5, 2011

book/doctrine.rst
+
+.. code-block:: bash
+
+ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
+
+This command makes sure that all of the getters and setters are generated
+for the ``Product`` class. This is a safe command - you can run it over and
+over again: it only generates getters and setters that don't exist (i.e. it
+doesn't replace your existing methods).
+
+.. note::
+
+ Doctrine doesn't care whether your properties are ``public``, ``protected``
+ or ``private``, or whether or not you have a getter or setter function
+ for a property. The getters and setters are generated here only because
+ you'll need them to interact with your PHP object.
@stof

stof Jun 5, 2011

Member

You should note somewhere that public properties are forbidden with Doctrine as they break the lazy-loading (the proxy cannot hook into the property access).

@weaverryan

weaverryan Jun 5, 2011

Member

I didn't realize that, but this makes perfect sense. So, since this only applies to relations, can you still use public properties for non-relational properties?

@stof

stof Jun 5, 2011

Member

No because the proxy is not loaded when you retrieve the related object but when you access to the object. Retrieving the object gives you the proxy instance as it corresponds to what you asked for.

@stof stof commented on an outdated diff Jun 5, 2011

book/doctrine.rst
+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.
+
+ DIAGRAM here of Product with related category on left, and on the right,
+ a diagram showing how the two tables are related.
@stof

stof Jun 5, 2011

Member

missing diagram here too

@stof stof commented on an outdated diff Jun 5, 2011

book/doctrine.rst
+ $categoryName = $product->getCategory()->getName();
+
+ // ...
+ }
+
+In this example, you first query for a ``Product`` object based on the product's
+``id``. This issues a query for *just* the product data and hydrates the
+``$product`` object with that data. Later, when you call ``$product->getCategory()->getName()``,
+Doctrine silently makes a second query to find the ``Category`` that's related
+to this ``Product``. It prepares the ``$category`` object and returns it to
+you.
+
+ DIAGRAM of querying for the Product object (on left) getting it from
+ db (on the right). You should then see the category_id of that Product.
+ Finally, we call ->getCategory() (on left) and it fetches the Category
+ from the DB (on the right).
@stof

stof Jun 5, 2011

Member

same here.

@stof stof commented on an outdated diff Jun 5, 2011

cookbook/doctrine/common_extensions.rst
@@ -0,0 +1,14 @@
+Doctrine Extensions: Timestampable: Sluggable, Translatable, etc.
+=================================================================
+
+Doctrine2 is very flexible, and the community has already created a series
+of useful Doctrine extensions to help you with tasks common entity-related
+tasks.
+
+One bundle in particular - the `DoctrineExtensionsBundle`_ - provides integration
+with an extensions library that offers *Sluggable*, *Translatable*, *Timestampable*,
+*Loggable*, and *Tree* behaviors.
+
@stof

stof Jun 5, 2011

Member

you could maybe add a link to the extensions as well.

@stof stof commented on the diff Jun 5, 2011

cookbook/doctrine/dbal.rst
+ Read the official Doctrine `DBAL Documentation`_ to learn all the details
+ and capabilities of Doctrine's DBAL library.
+
+To get started, configure the database connection parameters:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # app/config/config.yml
+ doctrine:
+ dbal:
+ driver: pdo_mysql
+ dbname: Symfony2
+ user: root
+ password: null
@stof

stof Jun 5, 2011

Member

you should add charset: UTF8 as this is needed to use MySQL with an UTF-8 encoding, which is a common use case

@stof stof and 2 others commented on an outdated diff Jun 5, 2011

cookbook/doctrine/event_listeners_subscribers.rst
@@ -0,0 +1,71 @@
+.. _doctrine-event-config:
+
+Registering Event Listeners and Subscribers
+===========================================
+
+.. caution::
+
+ This cookbook article is out of date and needs to be updated.
@stof

stof Jun 5, 2011

Member

is it really out of date ?

@fabpot

fabpot Jun 5, 2011

Owner

I don't think so. I've updated it, so the content should be up to date.

@weaverryan

weaverryan Jun 5, 2011

Member

I've removed the note - I had thought that I grep'ed symfony for the events found here and found nothing, but I was mistaken.

weaverryan added a commit that referenced this pull request Jun 5, 2011

Merge pull request #366 from symfony/doctrine
The new Doctrine chapter has landed

@weaverryan weaverryan merged commit 57650f7 into master Jun 5, 2011

Member

weaverryan commented Jun 5, 2011

I've merged the PR so that it makes it into the next docs build. But I'm happy to address any other comments (and the diagrams were just added).

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment