Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,95 @@ and directly start using native lazy objects.

# Upgrade to 3.6

## Deprecate using string expression for default values in mappings

Using a string expression for default values in field mappings is deprecated.
Use `Doctrine\DBAL\Schema\DefaultExpression` instances instead.

Here is how to address this deprecation when mapping entities using PHP attributes:

```diff
use DateTime;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentDate;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTime;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
final class TimeEntity
{
#[ORM\Id]
#[ORM\Column]
public int $id;

- #[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
public DateTime $createdAt;

- #[ORM\Column(options: ['default' => 'CURRENT_TIME'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentTime()], insertable: false, updatable: false)]
public DateTime $createdTime;

- #[ORM\Column(options: ['default' => 'CURRENT_DATE'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentDate()], insertable: false, updatable: false)]
public DateTime $createdDate;
}
```

Here is how to do the same when mapping entities using XML:

```diff
<?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
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

<entity name="Doctrine\Tests\ORM\Functional\XmlTimeEntity">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>

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

<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_TIMESTAMP</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
+ </option>
</options>
</field>

<field name="createdTime" type="time" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_TIME</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
+ </option>
</options>
</field>
<field name="createdDate" type="date" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_DATE</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
+ </option>
</options>
</field>
</entity>
</doctrine-mapping>
```


## Deprecate `FieldMapping::$default`

The `default` property of `Doctrine\ORM\Mapping\FieldMapping` is deprecated and
Expand Down
16 changes: 16 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,22 @@ 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
Expand Down
5 changes: 5 additions & 0 deletions docs/en/reference/basic-mapping/DefaultValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace App\Entity;

use DateTime;
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;

Expand All @@ -12,4 +14,7 @@ class Message
{
#[Column(options: ['default' => 'Hello World!'])]
private string $text;

#[Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
private DateTime $createdAt;
}
7 changes: 7 additions & 0 deletions docs/en/reference/basic-mapping/default-values.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@
<option name="default">Hello World!</option>
</options>
</field>
<field name="createdAt" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
</option>
</options>
</field>
</entity>
</doctrine-mapping>
16 changes: 13 additions & 3 deletions doctrine-mapping.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,20 @@
</xs:restriction>
</xs:simpleType>

<xs:complexType name="object">
<xs:attribute name="class" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>

<xs:complexType name="option" mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="object" type="orm:object"/>
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this is the right place to put the new element. Currently, I can provide multiple <object> elements for a single <option>, and the document will still be valid.

            <options>
                <option name="default">
                    <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
                    <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
                </option>
            </options>

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know much about XSD, but my LLM does so I had it prevent this possibility. It's implemented as a choice of 1 element between <object>, and an unbounded sequence of what was previously under the existing xs:choice.

<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required"/>
<xs:anyAttribute namespace="##other"/>
Expand Down
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,7 @@ parameters:
path: src/Mapping/Driver/XmlDriver.php

-
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>\} given\.$#'
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>\} given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/Driver/XmlDriver.php
Expand Down Expand Up @@ -1471,7 +1471,7 @@ parameters:
path: src/Mapping/Driver/XmlDriver.php

-
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>, quoted\?\: bool\}\.$#'
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>, quoted\?\: bool\}\.$#'
identifier: assign.propertyType
count: 1
path: src/Mapping/Driver/XmlDriver.php
Expand Down
47 changes: 45 additions & 2 deletions src/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use SimpleXMLElement;

use function assert;
use function class_exists;
use function constant;
use function count;
use function defined;
Expand Down Expand Up @@ -656,15 +657,30 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad
* Parses (nested) option elements.
*
* @return mixed[] The options array.
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string>
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string|object>
*/
private function parseOptions(SimpleXMLElement|null $options): array
{
$array = [];

foreach ($options ?? [] as $option) {
$value = null;
if ($option->count()) {
$value = $this->parseOptions($option->children());
// Check if this option contains an <object> element
$children = $option->children();
$hasObjectElement = false;

foreach ($children as $child) {
if ($child->getName() === 'object') {
$value = $this->parseObjectElement($child);
$hasObjectElement = true;
break;
}
}

if (! $hasObjectElement) {
$value = $this->parseOptions($children);
}
} else {
$value = (string) $option;
}
Expand All @@ -684,6 +700,33 @@ private function parseOptions(SimpleXMLElement|null $options): array
return $array;
}

/**
* Parses an <object> element and returns the instantiated object.
*
* @param SimpleXMLElement $objectElement The XML element.
*
* @return object The instantiated object.
*
* @throws MappingException If the object specification is invalid.
* @throws InvalidArgumentException If the class does not exist.
*/
private function parseObjectElement(SimpleXMLElement $objectElement): object
{
$attributes = $objectElement->attributes();

if (! isset($attributes->class)) {
throw MappingException::missingRequiredOption('object', 'class');
}

$className = (string) $attributes->class;

if (! class_exists($className)) {
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $className));
}

return new $className();
}

/**
* Constructs a joinColumn mapping array based on the information
* found in the given SimpleXMLElement.
Expand Down
31 changes: 31 additions & 0 deletions src/Tools/SchemaTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
Expand Down Expand Up @@ -507,20 +508,50 @@ private function gatherColumn(
], true)
&& $options['default'] === $this->platform->getCurrentTimestampSQL()
) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for datetime fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp` instance instead.
DEPRECATION,
$this->platform->getCurrentTimestampSQL(),
);
$options['default'] = new CurrentTimestamp();
}

if (
in_array($mapping->type, [Types::TIME_MUTABLE, Types::TIME_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentTimeSQL()
) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for time fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTime` instance instead.
DEPRECATION,
$this->platform->getCurrentTimeSQL(),
);
$options['default'] = new CurrentTime();
}

if (
in_array($mapping->type, [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentDateSQL()
) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for date fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentDate` instance instead.
DEPRECATION,
$this->platform->getCurrentDateSQL(),
);
$options['default'] = new CurrentDate();
}
}
Expand Down
Loading