Back to Orm

Basic Mapping

docs/en/reference/basic-mapping.rst

3.6.325.3 KB
Original Source

Basic Mapping

This guide explains the basic mapping of entities and properties. After working through this guide you should know:

  • How to create PHP objects that can be saved to the database with Doctrine;
  • How to configure the mapping between columns on tables and properties on entities;
  • What Doctrine mapping types are;
  • Defining primary keys and how identifiers are generated by Doctrine;
  • How quoting of reserved symbols works in Doctrine.

Mapping of associations will be covered in the next chapter on :doc:Association Mapping <association-mapping>.

Creating Classes for the Database

Every PHP object that you want to save in the database using Doctrine is called an Entity. The term "Entity" describes objects that have an identity over many independent requests. This identity is usually achieved by assigning a unique identifier to an entity. In this tutorial the following Message PHP class will serve as the example Entity:

.. code-block:: php

<?php
class Message
{
    private $id;
    private $text;
    private $postedAt;
}

Because Doctrine is a generic library, it only knows about your entities because you will describe their existence and structure using mapping metadata, which is configuration that tells Doctrine how your entity should be stored in the database. The documentation will often speak of "mapping something", which means writing the mapping metadata that describes your entity.

Doctrine provides several different ways to specify object-relational mapping metadata:

  • :doc:Attributes <attributes-reference>
  • :doc:XML <xml-mapping>
  • :doc:PHP code <php-mapping>

This manual will usually show mapping metadata via attributes, though many examples also show the equivalent configuration in XML.

.. note::

All metadata drivers perform equally. Once the metadata of a class has been
read from the source (attributes, XML, etc.) it is stored in an instance
of the ``Doctrine\ORM\Mapping\ClassMetadata`` class which are
stored in the metadata cache.  If you're not using a metadata cache (not
recommended!) then the XML driver is the fastest.

Marking our Message class as an entity for Doctrine is straightforward:

.. configuration-block::

.. code-block:: attribute

    <?php
    use Doctrine\ORM\Mapping\Entity;

    #[Entity]
    class Message
    {
        // ...
    }

.. code-block:: xml

    <doctrine-mapping>
      <entity name="Message">
          <!-- ... -->
      </entity>
    </doctrine-mapping>

With no additional information, Doctrine expects the entity to be saved into a table with the same name as the class in our case Message. You can change this by configuring information about the table:

.. configuration-block::

.. code-block:: attribute

    <?php
    use Doctrine\ORM\Mapping\Entity;
    use Doctrine\ORM\Mapping\Table;

    #[Entity]
    #[Table(name: 'message')]
    class Message
    {
        // ...
    }

.. code-block:: xml

    <doctrine-mapping>
      <entity name="Message" table="message">
          <!-- ... -->
      </entity>
    </doctrine-mapping>

Now the class Message will be saved and fetched from the table message.

Property Mapping

The next step is mapping its properties to columns in the table.

To configure a property use the Column attribute. The type argument specifies the :ref:Doctrine Mapping Type <reference-mapping-types> to use for the field. If the type is not specified, string is used as the default.

.. configuration-block::

.. code-block:: attribute

    <?php
    use Doctrine\ORM\Mapping\Column;
    use Doctrine\DBAL\Types\Types;

    #[Entity]
    class Message
    {
        #[Column(type: Types::INTEGER)]
        private $id;
        #[Column(length: 140)]
        private $text;
        #[Column(name: 'posted_at', type: Types::DATETIME)]
        private $postedAt;
    }

.. code-block:: xml

    <doctrine-mapping>
      <entity name="Message">
        <field name="id" type="integer" />
        <field name="text" length="140" />
        <field name="postedAt" column="posted_at" type="datetime" />
      </entity>
    </doctrine-mapping>

When we don't explicitly specify a column name via the name option, Doctrine assumes the field name is also the column name. So in this example:

  • the id property will map to the column id using the type integer;
  • the text property will map to the column text with the default mapping type string;
  • the postedAt property will map to the posted_at column with the datetime type.

Here is a complete list of Columns attributes (all optional):

  • type (default: 'string'): The mapping type to use for the column.
  • name (default: name of property): The name of the column in the database.
  • length (default: 255): The length of the column in the database. Applies only if a string-valued column is used.
  • unique (default: false): Whether the column is a unique key.
  • nullable (default: false): Whether the column is nullable.
  • insertable (default: true): Whether the column should be inserted.
  • updatable (default: true): Whether the column should be updated.
  • generated (default: null): Whether the generated strategy should be 'NEVER', 'INSERT' and ALWAYS.
  • enumType (requires PHP 8.1 and doctrine/orm 2.11): The PHP enum class name to convert the database value into. See :ref:reference-enum-mapping.
  • precision (default: 0): The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum number of digits that are stored for the values.
  • scale (default: 0): The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number of digits to the right of the decimal point and must not be greater than precision.
  • columnDefinition: Allows to define a custom DDL snippet that is used to create the column. Warning: This normally confuses the :doc:SchemaTool <tools> to always detect the column as changed.
  • options: Key-value pairs of options that get passed to the underlying database platform when generating DDL statements.

Specifying default values


While it is possible to specify default values for properties in your
PHP class, Doctrine also allows you to specify default values for
database columns using the ``default`` key in the ``options`` array of
the ``Column`` attribute.

When using XML, you can specify object instances using the ``<object>``
element:

.. code-block:: xml

    <field name="createdAt" type="datetime" insertable="false" updatable="false">
        <options>
            <option name="default">
                <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
            </option>
        </options>
    </field>

The ``<object>`` element requires a ``class`` attribute specifying the
fully qualified class name to instantiate.

.. configuration-block::
   .. literalinclude:: basic-mapping/DefaultValues.php
       :language: attribute

   .. literalinclude:: basic-mapping/default-values.xml
       :language: xml

.. _reference-php-mapping-types:

PHP Types Mapping
_________________

.. versionadded:: 2.9

The column types can be inferred automatically from PHP's property types.
However, when the property type is nullable this has no effect on the ``nullable`` Column attribute.

These are the "automatic" mapping rules:

+-----------------------+-------------------------------+
| PHP property type     | Doctrine column type          |
+=======================+===============================+
| ``DateInterval``      | ``Types::DATEINTERVAL``       |
+-----------------------+-------------------------------+
| ``DateTime``          | ``Types::DATETIME_MUTABLE``   |
+-----------------------+-------------------------------+
| ``DateTimeImmutable`` | ``Types::DATETIME_IMMUTABLE`` |
+-----------------------+-------------------------------+
| ``array``             | ``Types::JSON``               |
+-----------------------+-------------------------------+
| ``bool``              | ``Types::BOOLEAN``            |
+-----------------------+-------------------------------+
| ``float``             | ``Types::FLOAT``              |
+-----------------------+-------------------------------+
| ``int``               | ``Types::INTEGER``            |
+-----------------------+-------------------------------+
| Any other type        | ``Types::STRING``             |
+-----------------------+-------------------------------+

.. versionadded:: 2.11

As of version 2.11 Doctrine can also automatically map typed properties using a
PHP 8.1 enum to set the right ``type`` and ``enumType``.

.. versionadded:: 2.14

Since version 2.14 you can specify custom typed field mapping between PHP type and DBAL type using ``Configuration``
and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.

:doc:`Read more about TypedFieldMapper <typedfieldmapper>`.

Property Hooks
--------------

.. versionadded:: 3.4

Doctrine supports mapping hooked properties as long as they have a backed property
and are not virtual.


.. configuration-block::

    .. code-block:: attribute

        <?php
        use Doctrine\ORM\Mapping\Column;
        use Doctrine\DBAL\Types\Types;

        #[Entity]
        class Message
        {
            #[Column(type: Types::INTEGER)]
            private $id;
            #[Column(type: Types::STRING)]
            public string $language = 'de' {
                // Override the "read" action with arbitrary logic.
                get => strtoupper($this->language);

                // Override the "write" action with arbitrary logic.
                set {
                    $this->language = strtolower($value);
                }
            }
        }

    .. code-block:: xml

        <doctrine-mapping>
          <entity name="Message">
            <field name="id" type="integer" />
            <field name="language" />
          </entity>
        </doctrine-mapping>

If you attempt to map a virtual property with ``#[Column]`` an exception will be thrown.

Some caveats apply to the use of property hooks, as they behave differently when accessing the property through
the entity or directly through DQL/EntityRepository. Because the property hook can modify the value of the property in a way
that value and raw value are different, you have to use the raw value representation when querying for the property.

.. code-block:: php

    <?php
    $queryBuilder = $entityManager->createQueryBuilder();
    $queryBuilder->select('m')
        ->from(Message::class, 'm')
        ->where('m.language = :language')
        ->setParameter('language', 'de'); // Use lower case here for raw value representation

    $query = $queryBuilder->getQuery();
    $result = $query->getResult();

    $messageRepository = $entityManager->getRepository(Message::class);
    $deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation

.. _reference-enum-mapping:

Mapping PHP Enums
-----------------

.. versionadded:: 2.11

Doctrine natively supports mapping PHP backed enums to database columns.
A backed enum is a PHP enum that the same scalar type (``string`` or ``int``)
assigned to each case. Doctrine stores the scalar value in the database and
converts it back to the enum instance when hydrating the entity.

Using ``enumType`` provides three main benefits:

- **Automatic conversion**: Doctrine handles the conversion in both directions
  transparently. When loading an entity, scalar values from the database are
  converted into enum instances. When persisting, enum instances are reduced
  to their scalar ``->value`` before being sent to the database.
- **Type-safety**: Entity properties contain enum instances directly. Your
  getters return ``Suit`` instead of ``string``, removing the need to call
  ``Suit::from()`` manually.
- **Validation**: When a database value does not match any enum case, Doctrine
  throws a ``MappingException`` during hydration instead of silently returning
  an invalid value.

This feature works with all database platforms supported by Doctrine (MySQL,
PostgreSQL, SQLite, etc.) as it relies on standard column types (``string``,
``integer``, ``json``, ``simple_array``) rather than any vendor-specific enum
type.

.. note::

    This is unrelated to the MySQL-specific ``ENUM`` column type covered in
    :doc:`the MySQL Enums cookbook entry </cookbook/mysql-enums>`.

Defining an Enum
~~~~~~~~~~~~~~~~

.. literalinclude:: basic-mapping/Suit.php
    :language: php

Only backed enums (``string`` or ``int``) are supported. Unit enums (without
a scalar value) cannot be mapped.

Single-Value Columns
~~~~~~~~~~~~~~~~~~~~

Use the ``enumType`` option on ``#[Column]`` to map a property to a backed enum.
The underlying database column stores the enum's scalar value (``string`` or ``int``).

.. literalinclude:: basic-mapping/EnumMapping.php
    :language: php

When the PHP property is typed with the enum class, Doctrine automatically
infers the appropriate column type (``string`` for string-backed enums,
``integer`` for int-backed enums) and sets ``enumType``. You can also specify
the column ``type`` explicitly.

Storing Collections of Enums

You can store multiple enum values in a single column by combining enumType with a collection column type: json or simple_array.

.. note::

Automatic type inference does not apply to collection columns. When the
PHP property is typed as ``array``, Doctrine cannot detect the enum class.
You must specify both ``type`` and ``enumType`` explicitly.

.. literalinclude:: basic-mapping/EnumCollectionMapping.php :language: php

With json, the values are stored as a JSON array (e.g. ["hearts","spades"]). With simple_array, the values are stored as a comma-separated string (e.g. hearts,spades).

In both cases, Doctrine converts each element to and from the enum automatically during hydration and persistence.

.. tip::

Use ``json`` when enum values may contain commas, when you need to store
int-backed enums (as it preserves value types), when the column also
stores complex/nested data structures, or when you want to query individual
values using database-native JSON operators (e.g. PostgreSQL ``jsonb``).
Prefer ``simple_array`` for a compact, human-readable storage of
string-backed enums whose values do not contain commas.

+-------------------+-----------------------------+-------------------------------+ | Column type | Database storage | PHP type | +===================+=============================+===============================+ | string | hearts | Suit | +-------------------+-----------------------------+-------------------------------+ | integer | 1 | Priority | +-------------------+-----------------------------+-------------------------------+ | json | ["hearts","spades"] | array<Suit> | +-------------------+-----------------------------+-------------------------------+ | simple_array | hearts,spades | array<Suit> | +-------------------+-----------------------------+-------------------------------+

Nullable Enums


Enum columns can be nullable. When the database value is ``NULL``, Doctrine
preserves it as ``null`` without triggering any validation error.

.. code-block:: php

    <?php
    #[ORM\Column(type: 'string', nullable: true, enumType: Suit::class)]
    private Suit|null $suit = null;

Default Values

You can specify a database-level default using an enum case directly in the column options:

.. code-block:: php

<?php
#[ORM\Column(options: ['default' => Suit::Hearts])]
public Suit $suit;

Using Enums in Queries


Enum instances can be used directly as parameters in DQL, QueryBuilder, and
repository methods. Doctrine converts them to their scalar value automatically.

.. code-block:: php

    <?php
    // QueryBuilder
    $qb = $em->createQueryBuilder();
    $qb->select('c')
        ->from(Card::class, 'c')
        ->where('c.suit = :suit')
        ->setParameter('suit', Suit::Clubs);

    // Repository
    $cards = $em->getRepository(Card::class)->findBy(['suit' => Suit::Clubs]);

XML Mapping
~~~~~~~~~~~

When using XML mapping, the ``enum-type`` attribute is used on ``<field>``
elements:

.. code-block:: xml

    <field name="suit" type="string" enum-type="App\Entity\Suit" />

.. _reference-mapping-types:

Doctrine Mapping Types
----------------------

The ``type`` option used in the ``@Column`` accepts any of the
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
or :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`. A Doctrine type defines
the conversion between PHP and SQL types, independent from the database vendor
you are using.

.. note::

    DateTime and Object types are compared by reference, not by value. Doctrine
    updates this values if the reference changes and therefore behaves as if
    these objects are immutable value objects.

.. warning::

    All Date types assume that you are exclusively using the default timezone
    set by `date_default_timezone_set() <https://php.net/manual/en/function.date-default-timezone-set.php>`_
    or by the php.ini configuration ``date.timezone``. Working with
    different timezones will cause troubles and unexpected behavior.

    If you need specific timezone handling you have to handle this
    in your domain, converting all the values back and forth from UTC.
    There is also a :doc:`cookbook entry <../cookbook/working-with-datetime>`
    on working with datetimes that gives hints for implementing
    multi timezone applications.

Identifiers / Primary Keys
--------------------------

Every entity class must have an identifier/primary key. You can select
the field that serves as the identifier with the ``#[Id]`` attribute.

.. configuration-block::

    .. code-block:: attribute

        <?php
        class Message
        {
            #[Id]
            #[Column(type: 'integer')]
            #[GeneratedValue]
            private int|null $id = null;
            // ...
        }

    .. code-block:: xml

        <doctrine-mapping>
          <entity name="Message">
            <id name="id" type="integer">
                <generator strategy="AUTO" />
            </id>
            <!-- -->
          </entity>
        </doctrine-mapping>

In most cases using the automatic generator strategy (``#[GeneratedValue]``) is
what you want, but for backwards-compatibility reasons it might not. It
defaults to the identifier generation mechanism your current database
vendor preferred at the time that strategy was introduced:
``AUTO_INCREMENT`` with MySQL, sequences with PostgreSQL and Oracle and
so on.
If you are using `doctrine/dbal` 4, we now recommend using ``IDENTITY``
for PostgreSQL, and ``AUTO`` resolves to it because of that.
You can stick with ``SEQUENCE`` while still using the ``AUTO``
strategy, by configuring what it defaults to.

.. code-block:: php

    <?php
    use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
    use Doctrine\ORM\Configuration;

    $config = new Configuration();
    $config->setIdentityGenerationPreferences([
        PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
    ]);

.. _identifier-generation-strategies:

Identifier Generation Strategies

The previous example showed how to use the default identifier generation strategy without knowing the underlying database with the AUTO-detection strategy. It is also possible to specify the identifier generation strategy more explicitly, which allows you to make use of some additional features.

Here is the list of possible generation strategies:

  • AUTO (default): Tells Doctrine to pick the strategy that is preferred by the used database platform. The preferred strategies are IDENTITY for MySQL, SQLite, MsSQL, SQL Anywhere and PostgreSQL (on DBAL 4) and, for historical reasons, SEQUENCE for Oracle and PostgreSQL (on DBAL 3). This strategy provides full portability.
  • IDENTITY: Tells Doctrine to use special identity columns in the database that generate a value on insertion of a row. This strategy does currently not provide full portability and is supported by the following platforms: MySQL/SQLite/SQL Anywhere (AUTO_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL on DBAL 3, GENERATED BY DEFAULT AS IDENTITY on DBAL 4).
  • SEQUENCE: Tells Doctrine to use a database sequence for ID generation. This strategy does currently not provide full portability. Sequences are supported by Oracle, PostgreSQL and SQL Anywhere.
  • NONE: Tells Doctrine that the identifiers are assigned (and thus generated) by your code. The assignment must take place before a new entity is passed to EntityManager#persist. NONE is the same as leaving off the #[GeneratedValue] entirely.
  • CUSTOM: With this option, you can use the #[CustomIdGenerator] attribute. It will allow you to pass a :ref:class of your own to generate the identifiers. <attrref_customidgenerator>

Sequence Generator ^^^^^^^^^^^^^^^^^^

The Sequence Generator can currently be used in conjunction with Oracle or Postgres and allows some additional configuration options besides specifying the sequence's name:

.. configuration-block::

.. code-block:: attribute

    <?php
    class Message
    {
        #[Id]
        #[GeneratedValue(strategy: 'SEQUENCE')]
        #[SequenceGenerator(sequenceName: 'message_seq', initialValue: 1, allocationSize: 100)]
        protected int|null $id = null;
        // ...
    }

.. code-block:: xml

    <doctrine-mapping>
      <entity name="Message">
        <id name="id" type="integer">
            <generator strategy="SEQUENCE" />
            <sequence-generator sequence-name="message_seq" allocation-size="100" initial-value="1" />
        </id>
      </entity>
    </doctrine-mapping>

The initial value specifies at which value the sequence should start.

The allocationSize is a powerful feature to optimize INSERT performance of Doctrine. The allocationSize specifies by how much values the sequence is incremented whenever the next value is retrieved. If this is larger than 1 (one) Doctrine can generate identifier values for the allocationSizes amount of entities. In the above example with allocationSize=100 Doctrine ORM would only need to access the sequence once to generate the identifiers for 100 new entities.

.. caution::

The allocationSize is detected by SchemaTool and
transformed into an "INCREMENT BY " clause in the CREATE SEQUENCE
statement. For a database schema created manually (and not
SchemaTool) you have to make sure that the allocationSize
configuration option is never larger than the actual sequences
INCREMENT BY value, otherwise you may get duplicate keys.

.. note::

It is possible to use strategy="AUTO" and at the same time
specifying a @SequenceGenerator. In such a case, your custom
sequence settings are used in the case where the preferred strategy
of the underlying platform is SEQUENCE, such as for Oracle and
PostgreSQL.

Composite Keys


With Doctrine ORM you can use composite primary keys, using ``#[Id]`` on
more than one column. Some restrictions exist opposed to using a single
identifier in this case: The use of the ``#[GeneratedValue]`` attribute
is not supported, which means you can only use composite keys if you
generate the primary key values yourself before calling
``EntityManager#persist()`` on the entity.

More details on composite primary keys are discussed in a :doc:`dedicated tutorial
<../tutorials/composite-primary-keys>`.

Quoting Reserved Words
----------------------

Sometimes it is necessary to quote a column or table name because of reserved
word conflicts. Doctrine does not quote identifiers automatically, because it
leads to more problems than it would solve. Quoting tables and column names
needs to be done explicitly using ticks in the definition.

.. code-block:: php

    <?php

    #[Column(name: '`number`', type: 'integer')]
    private $number;

Doctrine will then quote this column name in all SQL statements
according to the used database platform.

.. warning::

    Identifier Quoting does not work for join column names or discriminator
    column names unless you are using a custom ``QuoteStrategy``.

.. _reference-basic-mapping-custom-mapping-types:

For more control over column quoting the ``Doctrine\ORM\Mapping\QuoteStrategy`` interface
was introduced in ORM. It is invoked for every column, table, alias and other
SQL names. You can implement the QuoteStrategy and set it by calling
``Doctrine\ORM\Configuration#setQuoteStrategy()``.

The ANSI Quote Strategy was added, which assumes quoting is not necessary for any SQL name.
You can use it with the following code:

.. code-block:: php

    <?php
    use Doctrine\ORM\Mapping\AnsiQuoteStrategy;

    $configuration->setQuoteStrategy(new AnsiQuoteStrategy());