diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 58c53f5ae38..f0bbf5b9e2a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -40,9 +40,5 @@ jobs: with: dependency-versions: "highest" - - name: "Add dummy title to the sidebar" - run: | - printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst - - name: "Run guides-cli" - run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'Unknown directive' | ( ! grep WARNING )" + run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 025f29ea02d..32478b18cc1 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,7 +10,7 @@ on: - src/** - phpstan* - psalm* - - tests/Doctrine/StaticAnalysis/** + - tests/StaticAnalysis/** push: branches: - "*.x" @@ -20,7 +20,7 @@ on: - src/** - phpstan* - psalm* - - tests/Doctrine/StaticAnalysis/** + - tests/StaticAnalysis/** jobs: static-analysis-phpstan: @@ -32,7 +32,7 @@ jobs: include: - dbal-version: default config: phpstan.neon - - dbal-version: 3.7 + - dbal-version: 3.8.2 config: phpstan-dbal3.neon steps: @@ -65,7 +65,7 @@ jobs: matrix: dbal-version: - default - - 3.7 + - 3.8.2 steps: - name: "Checkout code" diff --git a/UPGRADE.md b/UPGRADE.md index b9a6586ab70..fbf8ce85349 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -37,9 +37,9 @@ so `$targetEntity` is a first argument now. This change affects only non-named a When using the `AUTO` strategy to let Doctrine determine the identity generation mechanism for an entity, and when using `doctrine/dbal` 4, PostgreSQL now uses `IDENTITY` -instead of `SEQUENCE`. When upgrading from ORM 2.x and preference is on keeping -the `SEQUENCE` based identity generation, then configure the ORM this way: - +instead of `SEQUENCE` or `SERIAL`. +* If you want to upgrade your existing tables to identity columns, you will need to follow [migration to identity columns on PostgreSQL](https://www.doctrine-project.org/projects/doctrine-dbal/en/4.0/how-to/postgresql-identity-migration.html) +* If you want to keep using SQL sequences, you need to configure the ORM this way: ```php use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\ORM\Configuration; diff --git a/composer.json b/composer.json index 6aefbec39e4..2a002989c21 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "composer-runtime-api": "^2", "ext-ctype": "*", "doctrine/collections": "^2.1", - "doctrine/dbal": "^3.6 || ^4", + "doctrine/dbal": "^3.8.2 || ^4", "doctrine/deprecations": "^0.5.3 || ^1", "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", diff --git a/docs/en/cookbook/implementing-arrayaccess-for-domain-objects.rst b/docs/en/cookbook/implementing-arrayaccess-for-domain-objects.rst index 363d1ad8ba1..40ca4fbdfc5 100644 --- a/docs/en/cookbook/implementing-arrayaccess-for-domain-objects.rst +++ b/docs/en/cookbook/implementing-arrayaccess-for-domain-objects.rst @@ -1,7 +1,7 @@ Implementing ArrayAccess for Domain Objects =========================================== -.. sectionauthor:: Roman Borschel (roman@code-factory.org) +.. sectionauthor:: Roman Borschel This recipe will show you how to implement ArrayAccess for your domain objects in order to allow more uniform access, for example diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 9624b9fb4dd..49aacf93046 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -377,7 +377,7 @@ Here is the list of possible generation strategies: 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.` + It will allow you to pass a :ref:`class of your own to generate the identifiers. ` Sequence Generator ^^^^^^^^^^^^^^^^^^ diff --git a/docs/en/reference/batch-processing.rst b/docs/en/reference/batch-processing.rst index 12cac934164..ee381bbe5c4 100644 --- a/docs/en/reference/batch-processing.rst +++ b/docs/en/reference/batch-processing.rst @@ -18,14 +18,20 @@ especially what the strategies presented here provide help with. .. note:: - Having an SQL logger enabled when processing batches can have a serious impact on performance and resource usage. - To avoid that you should remove the corresponding middleware. - To remove all middlewares, you can use this line: + Having an SQL logger enabled when processing batches can have a + serious impact on performance and resource usage. + To avoid that, you should use a PSR logger implementation that can be + disabled at runtime. + For example, with Monolog, you can use ``Logger::pushHandler()`` + to push a ``NullHandler`` to the logger instance, and then pop it + when you need to enable logging again. + + With DBAL 2, you can disable the SQL logger like below: + .. code-block:: php getConnection()->getConfiguration()->setMiddlewares([]); // DBAL 3 - $em->getConnection()->getConfiguration()->setSQLLogger(null); // DBAL 2 + $em->getConnection()->getConfiguration()->setSQLLogger(null); Bulk Inserts ------------ @@ -188,6 +194,3 @@ problems using the following approach: Iterating results is not possible with queries that fetch-join a collection-valued association. The nature of such SQL result sets is not suitable for incremental hydration. - - - diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index a4bf1e14ce0..4211a198577 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -131,47 +131,47 @@ There are two ways to set up an event handler: * For *all events* you can create a Lifecycle Event Listener or Subscriber class and register it by calling ``$eventManager->addEventListener()`` or ``eventManager->addEventSubscriber()``, see -:ref:`Listening and subscribing to Lifecycle Events` +:ref:`Listening and subscribing to Lifecycle Events ` * For *some events* (see table below), you can create a *Lifecycle Callback* method in the -entity, see :ref:`Lifecycle Callbacks`. +entity, see :ref:`Lifecycle Callbacks `. .. _reference-events-lifecycle-events: Events Overview --------------- -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| Event | Dispatched by | Lifecycle | Passed | -| | | Callback | Argument | -+=================================================================+=======================+===========+=====================================+ -| :ref:`preRemove` | ``$em->remove()`` | Yes | `PreRemoveEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`postRemove` | ``$em->flush()`` | Yes | `PostRemoveEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`prePersist` | ``$em->persist()`` | Yes | `PrePersistEventArgs`_ | -| | on *initial* persist | | | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`postPersist` | ``$em->flush()`` | Yes | `PostPersistEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`preUpdate` | ``$em->flush()`` | Yes | `PreUpdateEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`postUpdate` | ``$em->flush()`` | Yes | `PostUpdateEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`postLoad` | Loading from database | Yes | `PostLoadEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`loadClassMetadata` | Loading of mapping | No | `LoadClassMetadataEventArgs`_ | -| | metadata | | | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| ``onClassMetadataNotFound`` | ``MappingException`` | No | `OnClassMetadataNotFoundEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`preFlush` | ``$em->flush()`` | Yes | `PreFlushEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`onFlush` | ``$em->flush()`` | No | `OnFlushEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`postFlush` | ``$em->flush()`` | No | `PostFlushEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ -| :ref:`onClear` | ``$em->clear()`` | No | `OnClearEventArgs`_ | -+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| Event | Dispatched by | Lifecycle | Passed | +| | | Callback | Argument | ++==================================================================+=======================+===========+=====================================+ +| :ref:`preRemove ` | ``$em->remove()`` | Yes | `PreRemoveEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`postRemove ` | ``$em->flush()`` | Yes | `PostRemoveEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`prePersist ` | ``$em->persist()`` | Yes | `PrePersistEventArgs`_ | +| | on *initial* persist | | | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`postPersist ` | ``$em->flush()`` | Yes | `PostPersistEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`preUpdate ` | ``$em->flush()`` | Yes | `PreUpdateEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`postUpdate ` | ``$em->flush()`` | Yes | `PostUpdateEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`postLoad ` | Loading from database | Yes | `PostLoadEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`loadClassMetadata ` | Loading of mapping | No | `LoadClassMetadataEventArgs`_ | +| | metadata | | | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| ``onClassMetadataNotFound`` | ``MappingException`` | No | `OnClassMetadataNotFoundEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`preFlush ` | ``$em->flush()`` | Yes | `PreFlushEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`onFlush ` | ``$em->flush()`` | No | `OnFlushEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`postFlush ` | ``$em->flush()`` | No | `PostFlushEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ +| :ref:`onClear ` | ``$em->clear()`` | No | `OnClearEventArgs`_ | ++------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+ .. warning:: @@ -313,7 +313,7 @@ behaviors across different entity classes. Note that they require much more detailed knowledge about the inner workings of the ``EntityManager`` and ``UnitOfWork`` classes. Please -read the :ref:`Implementing Event Listeners` section +read the :ref:`Implementing Event Listeners ` section carefully if you are trying to write your own listener. For event subscribers, there are no surprises. They declare the @@ -426,11 +426,11 @@ prePersist There are two ways for the ``prePersist`` event to be triggered: - One is when you call ``EntityManager::persist()``. The - event is also called for all :ref:`cascaded associations`. + event is also called for all :ref:`cascaded associations `. - The other is inside the ``flush()`` method when changes to associations are computed and - this association is marked as :ref:`cascade: persist`. Any new entity found + this association is marked as :ref:`cascade: persist `. Any new entity found during this operation is also persisted and ``prePersist`` called - on it. This is called :ref:`persistence by reachability`. + on it. This is called :ref:`persistence by reachability `. In both cases you get passed a ``PrePersistEventArgs`` instance which has access to the entity and the entity manager. @@ -454,7 +454,7 @@ preRemove The ``preRemove`` event is called on every entity immediately when it is passed to the ``EntityManager::remove()`` method. It is cascaded for all -associations that are marked as :ref:`cascade: remove` +associations that are marked as :ref:`cascade: remove ` It is not called for a DQL ``DELETE`` statement. @@ -502,7 +502,7 @@ entities and their associations have been computed. This means, the - Collections scheduled for removal To make use of the ``onFlush`` event you have to be familiar with the -internal :ref:`UnitOfWork` API, which grants you access to the previously +internal :ref:`UnitOfWork ` API, which grants you access to the previously mentioned sets. See this example: .. code-block:: php diff --git a/docs/en/reference/faq.rst b/docs/en/reference/faq.rst index a81812a9c2b..85b6e35d851 100644 --- a/docs/en/reference/faq.rst +++ b/docs/en/reference/faq.rst @@ -101,7 +101,7 @@ The many-to-many association is only supporting foreign keys in the table defini To work with many-to-many tables containing extra columns you have to use the foreign keys as primary keys feature of Doctrine ORM. -See :doc:`the tutorial on composite primary keys for more information<../tutorials/composite-primary-keys>`. +See :doc:`the tutorial on composite primary keys for more information <../tutorials/composite-primary-keys>`. How can i paginate fetch-joined collections? diff --git a/docs/en/reference/inheritance-mapping.rst b/docs/en/reference/inheritance-mapping.rst index 399580b9702..38696dd22a0 100644 --- a/docs/en/reference/inheritance-mapping.rst +++ b/docs/en/reference/inheritance-mapping.rst @@ -342,7 +342,7 @@ It is not supported to use overrides in entity inheritance scenarios. .. note:: When using traits, make sure not to miss the warnings given in the - :doc:`Limitations and Known Issues` chapter. + :doc:`Limitations and Known Issues ` chapter. Association Override diff --git a/docs/en/reference/installation.rst b/docs/en/reference/installation.rst index dab1364f777..2c0d5823009 100644 --- a/docs/en/reference/installation.rst +++ b/docs/en/reference/installation.rst @@ -1,3 +1,5 @@ +:orphan: + Installation ============ diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 4dfc08c4540..619c6e2511a 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -1,3 +1,5 @@ +:orphan: + .. toc:: .. tocheader:: Tutorials @@ -31,6 +33,7 @@ reference/inheritance-mapping reference/working-with-objects reference/working-with-associations + reference/typedfieldmapper reference/events reference/unitofwork reference/unitofwork-associations diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 17a090f3a2c..688463b1bb2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1042,10 +1042,6 @@ - $having - $having - $where - $where $join]]]> $join]]]> diff --git a/src/Internal/Hydration/ObjectHydrator.php b/src/Internal/Hydration/ObjectHydrator.php index c83c1e4701e..d24323d8689 100644 --- a/src/Internal/Hydration/ObjectHydrator.php +++ b/src/Internal/Hydration/ObjectHydrator.php @@ -91,7 +91,7 @@ protected function prepare(): void } // handle fetch-joined owning side bi-directional one-to-one associations - if ($assoc->inversedBy) { + if ($assoc->inversedBy !== null) { $class = $this->getClassMetadata($className); $inverseAssoc = $class->associationMappings[$assoc->inversedBy]; @@ -439,7 +439,7 @@ protected function hydrateRowData(array $row, array &$result): void if ($relation->isOwningSide()) { // TODO: Just check hints['fetched'] here? // If there is an inverse mapping on the target class its bidirectional - if ($relation->inversedBy) { + if ($relation->inversedBy !== null) { $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy]; if ($inverseAssoc->isToOne()) { $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject); diff --git a/src/Internal/NoUnknownNamedArguments.php b/src/Internal/NoUnknownNamedArguments.php new file mode 100644 index 00000000000..7584744c162 --- /dev/null +++ b/src/Internal/NoUnknownNamedArguments.php @@ -0,0 +1,55 @@ + $parameter + */ + private static function validateVariadicParameter(array $parameter): void + { + if (array_is_list($parameter)) { + return; + } + + [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + assert(isset($trace['class'])); + + $additionalArguments = array_values(array_filter( + array_keys($parameter), + is_string(...), + )); + + throw new BadMethodCallException(sprintf( + 'Invalid call to %s::%s(), unknown named arguments: %s', + $trace['class'], + $trace['function'], + implode(', ', $additionalArguments), + )); + } +} diff --git a/src/Mapping/Builder/ClassMetadataBuilder.php b/src/Mapping/Builder/ClassMetadataBuilder.php index 0f208f29139..b9d3cc81f5e 100644 --- a/src/Mapping/Builder/ClassMetadataBuilder.php +++ b/src/Mapping/Builder/ClassMetadataBuilder.php @@ -288,7 +288,7 @@ public function addManyToOne( ): ClassMetadataBuilder { $builder = $this->createManyToOne($name, $targetEntity); - if ($inversedBy) { + if ($inversedBy !== null) { $builder->inversedBy($inversedBy); } @@ -348,7 +348,7 @@ public function addOwningOneToOne( ): ClassMetadataBuilder { $builder = $this->createOneToOne($name, $targetEntity); - if ($inversedBy) { + if ($inversedBy !== null) { $builder->inversedBy($inversedBy); } @@ -380,7 +380,7 @@ public function addOwningManyToMany( ): ClassMetadataBuilder { $builder = $this->createManyToMany($name, $targetEntity); - if ($inversedBy) { + if ($inversedBy !== null) { $builder->inversedBy($inversedBy); } diff --git a/src/Mapping/ChainTypedFieldMapper.php b/src/Mapping/ChainTypedFieldMapper.php index 85e7faa6e4c..ed1ba93706c 100644 --- a/src/Mapping/ChainTypedFieldMapper.php +++ b/src/Mapping/ChainTypedFieldMapper.php @@ -4,18 +4,20 @@ namespace Doctrine\ORM\Mapping; +use Doctrine\ORM\Internal\NoUnknownNamedArguments; use ReflectionProperty; final class ChainTypedFieldMapper implements TypedFieldMapper { - /** - * @readonly - * @var TypedFieldMapper[] $typedFieldMappers - */ + use NoUnknownNamedArguments; + + /** @var list $typedFieldMappers */ private readonly array $typedFieldMappers; public function __construct(TypedFieldMapper ...$typedFieldMappers) { + self::validateVariadicParameter($typedFieldMappers); + $this->typedFieldMappers = $typedFieldMappers; } diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index e6b09774408..679e8a17069 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -764,7 +764,7 @@ public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEnti $targetClass = $this->em->getClassMetadata($assoc->targetEntity); if ($assoc->isOwningSide()) { - $isInverseSingleValued = $assoc->inversedBy && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy); + $isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy); // Mark inverse side as fetched in the hints, otherwise the UoW would // try to load it in a separate query (remember: to-one inverse sides can not be lazy). diff --git a/src/Query/AST/Functions/DateAddFunction.php b/src/Query/AST/Functions/DateAddFunction.php index 385ebac99c9..12920dcbd0f 100644 --- a/src/Query/AST/Functions/DateAddFunction.php +++ b/src/Query/AST/Functions/DateAddFunction.php @@ -11,8 +11,6 @@ use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\TokenType; -use function assert; -use function is_numeric; use function strtolower; /** @@ -63,17 +61,10 @@ public function getSql(SqlWalker $sqlWalker): string }; } - /** - * @return numeric-string - * - * @throws ASTException - */ + /** @throws ASTException */ private function dispatchIntervalExpression(SqlWalker $sqlWalker): string { - $sql = $this->intervalExpression->dispatch($sqlWalker); - assert(is_numeric($sql)); - - return $sql; + return $this->intervalExpression->dispatch($sqlWalker); } public function parse(Parser $parser): void diff --git a/src/Query/AST/Functions/DateSubFunction.php b/src/Query/AST/Functions/DateSubFunction.php index 254f1219277..5363680e2a4 100644 --- a/src/Query/AST/Functions/DateSubFunction.php +++ b/src/Query/AST/Functions/DateSubFunction.php @@ -8,8 +8,6 @@ use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\SqlWalker; -use function assert; -use function is_numeric; use function strtolower; /** @@ -56,16 +54,9 @@ public function getSql(SqlWalker $sqlWalker): string }; } - /** - * @return numeric-string - * - * @throws ASTException - */ + /** @throws ASTException */ private function dispatchIntervalExpression(SqlWalker $sqlWalker): string { - $sql = $this->intervalExpression->dispatch($sqlWalker); - assert(is_numeric($sql)); - - return $sql; + return $this->intervalExpression->dispatch($sqlWalker); } } diff --git a/src/Query/AST/Functions/TrimFunction.php b/src/Query/AST/Functions/TrimFunction.php index f3f2fb9f3df..e0a3e99c356 100644 --- a/src/Query/AST/Functions/TrimFunction.php +++ b/src/Query/AST/Functions/TrimFunction.php @@ -59,7 +59,7 @@ public function parse(Parser $parser): void $this->trimChar = $lexer->token->value; } - if ($this->leading || $this->trailing || $this->both || $this->trimChar) { + if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) { $parser->match(TokenType::T_FROM); } diff --git a/src/Query/Expr.php b/src/Query/Expr.php index 0629156d837..65f30827f24 100644 --- a/src/Query/Expr.php +++ b/src/Query/Expr.php @@ -4,6 +4,7 @@ namespace Doctrine\ORM\Query; +use Doctrine\ORM\Internal\NoUnknownNamedArguments; use Traversable; use function implode; @@ -23,6 +24,8 @@ */ class Expr { + use NoUnknownNamedArguments; + /** * Creates a conjunction of the given boolean expressions. * @@ -38,6 +41,8 @@ class Expr */ public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx { + self::validateVariadicParameter($x); + return new Expr\Andx($x); } @@ -56,6 +61,8 @@ public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): */ public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx { + self::validateVariadicParameter($x); + return new Expr\Orx($x); } @@ -225,6 +232,8 @@ public function count(mixed $x): Expr\Func */ public function countDistinct(mixed ...$x): string { + self::validateVariadicParameter($x); + return 'COUNT(DISTINCT ' . implode(', ', $x) . ')'; } @@ -470,6 +479,8 @@ public function notLike(string $x, mixed $y): Expr\Comparison */ public function concat(mixed ...$x): Expr\Func { + self::validateVariadicParameter($x); + return new Expr\Func('CONCAT', $x); } diff --git a/src/Query/Expr/Base.php b/src/Query/Expr/Base.php index a98ea3b8cb3..e0f257277df 100644 --- a/src/Query/Expr/Base.php +++ b/src/Query/Expr/Base.php @@ -7,10 +7,12 @@ use InvalidArgumentException; use Stringable; +use function array_key_exists; use function count; use function get_debug_type; use function implode; use function in_array; +use function is_array; use function is_string; use function sprintf; @@ -33,6 +35,10 @@ abstract class Base implements Stringable public function __construct(mixed $args = []) { + if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { + $args = $args[0]; + } + $this->addMultiple($args); } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 115dea0187d..f79e2fe83f0 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -6,6 +6,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; +use Doctrine\ORM\Internal\NoUnknownNamedArguments; use Doctrine\ORM\Internal\QueryType; use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Parameter; @@ -38,6 +39,8 @@ */ class QueryBuilder implements Stringable { + use NoUnknownNamedArguments; + /** * The array of DQL parts collected. * @@ -611,6 +614,8 @@ public function add(string $dqlPartName, string|object|array $dqlPart, bool $app */ public function select(mixed ...$select): static { + self::validateVariadicParameter($select); + $this->type = QueryType::Select; if ($select === []) { @@ -657,6 +662,8 @@ public function distinct(bool $flag = true): static */ public function addSelect(mixed ...$select): static { + self::validateVariadicParameter($select); + $this->type = QueryType::Select; if ($select === []) { @@ -951,6 +958,8 @@ public function set(string $key, mixed $value): static */ public function where(mixed ...$predicates): static { + self::validateVariadicParameter($predicates); + if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) { $predicates = new Expr\Andx($predicates); } @@ -976,6 +985,8 @@ public function where(mixed ...$predicates): static */ public function andWhere(mixed ...$where): static { + self::validateVariadicParameter($where); + $dql = $this->getDQLPart('where'); if ($dql instanceof Expr\Andx) { @@ -1006,6 +1017,8 @@ public function andWhere(mixed ...$where): static */ public function orWhere(mixed ...$where): static { + self::validateVariadicParameter($where); + $dql = $this->getDQLPart('where'); if ($dql instanceof Expr\Orx) { @@ -1033,6 +1046,8 @@ public function orWhere(mixed ...$where): static */ public function groupBy(string ...$groupBy): static { + self::validateVariadicParameter($groupBy); + return $this->add('groupBy', new Expr\GroupBy($groupBy)); } @@ -1051,6 +1066,8 @@ public function groupBy(string ...$groupBy): static */ public function addGroupBy(string ...$groupBy): static { + self::validateVariadicParameter($groupBy); + return $this->add('groupBy', new Expr\GroupBy($groupBy), true); } @@ -1062,6 +1079,8 @@ public function addGroupBy(string ...$groupBy): static */ public function having(mixed ...$having): static { + self::validateVariadicParameter($having); + if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) { $having = new Expr\Andx($having); } @@ -1077,6 +1096,8 @@ public function having(mixed ...$having): static */ public function andHaving(mixed ...$having): static { + self::validateVariadicParameter($having); + $dql = $this->getDQLPart('having'); if ($dql instanceof Expr\Andx) { @@ -1097,6 +1118,8 @@ public function andHaving(mixed ...$having): static */ public function orHaving(mixed ...$having): static { + self::validateVariadicParameter($having); + $dql = $this->getDQLPart('having'); if ($dql instanceof Expr\Orx) { diff --git a/src/Tools/AttachEntityListenersListener.php b/src/Tools/AttachEntityListenersListener.php index 63e8f7b5a13..9203cfe782c 100644 --- a/src/Tools/AttachEntityListenersListener.php +++ b/src/Tools/AttachEntityListenersListener.php @@ -5,8 +5,10 @@ namespace Doctrine\ORM\Tools; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder; +use function assert; use function ltrim; /** @@ -14,16 +16,22 @@ */ class AttachEntityListenersListener { - /** @var mixed[][] */ + /** + * @var array> + */ private array $entityListeners = []; /** * Adds an entity listener for a specific entity. * - * @param string $entityClass The entity to attach the listener. - * @param string $listenerClass The listener class. - * @param string|null $eventName The entity lifecycle event. - * @param string|null $listenerCallback The listener callback method or NULL to use $eventName. + * @param class-string $entityClass The entity to attach the listener. + * @param class-string $listenerClass The listener class. + * @param Events::*|null $eventName The entity lifecycle event. + * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName. */ public function addEntityListener( string $entityClass, @@ -34,7 +42,7 @@ public function addEntityListener( $this->entityListeners[ltrim($entityClass, '\\')][] = [ 'event' => $eventName, 'class' => $listenerClass, - 'method' => $listenerCallback ?: $eventName, + 'method' => $listenerCallback ?? $eventName, ]; } @@ -53,6 +61,7 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void if ($listener['event'] === null) { EntityListenerBuilder::bindEntityListener($metadata, $listener['class']); } else { + assert($listener['method'] !== null); $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); } } diff --git a/src/Tools/SchemaValidator.php b/src/Tools/SchemaValidator.php index 43b10ee3983..6ebe991876c 100644 --- a/src/Tools/SchemaValidator.php +++ b/src/Tools/SchemaValidator.php @@ -162,7 +162,7 @@ public function validateClass(ClassMetadata $class): array } } - if ($assoc->isOwningSide() && $assoc->inversedBy) { + if ($assoc->isOwningSide() && $assoc->inversedBy !== null) { if ($targetMetadata->hasField($assoc->inversedBy)) { $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.'; diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 3b020c955f2..5c16c511ce1 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2550,7 +2550,7 @@ public function createEntity(string $className, array $data, array &$hints = []) $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); - if ($assoc->inversedBy && $assoc->isOneToOne() && $newValue !== null) { + if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) { $inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy]; $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity); } diff --git a/tests/Tests/ORM/ConfigurationTest.php b/tests/Tests/ORM/ConfigurationTest.php index 3e61fa89add..269228e72ea 100644 --- a/tests/Tests/ORM/ConfigurationTest.php +++ b/tests/Tests/ORM/ConfigurationTest.php @@ -4,7 +4,6 @@ namespace Doctrine\Tests\ORM; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Cache\CacheConfiguration; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityRepository; @@ -26,8 +25,6 @@ */ class ConfigurationTest extends TestCase { - use VerifyDeprecations; - private Configuration $configuration; protected function setUp(): void diff --git a/tests/Tests/ORM/EntityManagerTest.php b/tests/Tests/ORM/EntityManagerTest.php index 5abcffc67d6..067b1c6874b 100644 --- a/tests/Tests/ORM/EntityManagerTest.php +++ b/tests/Tests/ORM/EntityManagerTest.php @@ -6,7 +6,6 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Exception\EntityManagerClosed; @@ -27,8 +26,6 @@ class EntityManagerTest extends OrmTestCase { - use VerifyDeprecations; - private EntityManagerMock $entityManager; protected function setUp(): void diff --git a/tests/Tests/ORM/Functional/NativeQueryTest.php b/tests/Tests/ORM/Functional/NativeQueryTest.php index 6b8f883f418..c9f665387b7 100644 --- a/tests/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Tests/ORM/Functional/NativeQueryTest.php @@ -8,7 +8,6 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type as DBALType; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Internal\SQLResultCasing; use Doctrine\ORM\PersistentCollection; @@ -33,7 +32,6 @@ class NativeQueryTest extends OrmFunctionalTestCase { use SQLResultCasing; - use VerifyDeprecations; private AbstractPlatform|null $platform = null; diff --git a/tests/Tests/ORM/Functional/QueryDqlFunctionTest.php b/tests/Tests/ORM/Functional/QueryDqlFunctionTest.php index 9fdc7d64516..5a6aa5da3ab 100644 --- a/tests/Tests/ORM/Functional/QueryDqlFunctionTest.php +++ b/tests/Tests/ORM/Functional/QueryDqlFunctionTest.php @@ -7,6 +7,7 @@ use DateTimeImmutable; use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\AbstractQuery; +use Doctrine\Tests\Models\Company\CompanyEmployee; use Doctrine\Tests\Models\Company\CompanyManager; use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -487,4 +488,34 @@ protected function generateFixture(): void $this->_em->flush(); $this->_em->clear(); } + + #[Group('GH-11240')] + public function testDateAddWithColumnInterval(): void + { + $query = sprintf( + 'SELECT DATE_ADD(CURRENT_TIMESTAMP(), m.salary, \'day\') AS add FROM %s m', + CompanyEmployee::class, + ); + + $result = $this->_em->createQuery($query) + ->setMaxResults(1) + ->getSingleResult(AbstractQuery::HYDRATE_ARRAY); + + self::assertArrayHasKey('add', $result); + } + + #[Group('GH-11240')] + public function testDateSubWithColumnInterval(): void + { + $query = sprintf( + 'SELECT DATE_SUB(CURRENT_TIMESTAMP(), m.salary, \'day\') AS add FROM %s m', + CompanyEmployee::class, + ); + + $result = $this->_em->createQuery($query) + ->setMaxResults(1) + ->getSingleResult(AbstractQuery::HYDRATE_ARRAY); + + self::assertArrayHasKey('add', $result); + } } diff --git a/tests/Tests/ORM/Functional/Ticket/DDC117Test.php b/tests/Tests/ORM/Functional/Ticket/DDC117Test.php index 138d167d366..dba78b9afd7 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC117Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC117Test.php @@ -4,7 +4,6 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\UnitOfWork; use Doctrine\Tests\Models\DDC117\DDC117ApproveChanges; use Doctrine\Tests\Models\DDC117\DDC117Article; @@ -23,8 +22,6 @@ #[Group('DDC-117')] class DDC117Test extends OrmFunctionalTestCase { - use VerifyDeprecations; - private DDC117Article|null $article1; private DDC117Article|null $article2; diff --git a/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php b/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php index cade45f5a91..28642118e01 100644 --- a/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php +++ b/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php @@ -4,7 +4,6 @@ namespace Doctrine\Tests\ORM\Mapping; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Id\SequenceGenerator as IdSequenceGenerator; use Doctrine\ORM\Mapping\ClassMetadata; @@ -42,8 +41,6 @@ class BasicInheritanceMappingTest extends OrmTestCase { - use VerifyDeprecations; - private ClassMetadataFactory $cmf; protected function setUp(): void diff --git a/tests/Tests/ORM/Mapping/ClassMetadataFactoryTest.php b/tests/Tests/ORM/Mapping/ClassMetadataFactoryTest.php index 315dd203600..23054d4899e 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataFactoryTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataFactoryTest.php @@ -9,7 +9,6 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; @@ -55,8 +54,6 @@ class ClassMetadataFactoryTest extends OrmTestCase { - use VerifyDeprecations; - public function testGetMetadataForSingleClass(): void { $platform = $this->createMock(AbstractPlatform::class); diff --git a/tests/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Tests/ORM/Query/LanguageRecognitionTest.php index 72fc2a8d451..0bdf2f37b38 100644 --- a/tests/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Tests/ORM/Query/LanguageRecognitionTest.php @@ -162,6 +162,11 @@ public function testFunctionalExpressionsSupportedInWherePart(): void $this->assertValidDQL("SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM(u.name) = 'someone'"); } + public function testTrimFalsyString(): void + { + $this->assertValidDQL("SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM('0' FROM u.name) = 'someone'"); + } + public function testArithmeticExpressionsSupportedInWherePart(): void { $this->assertValidDQL('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000'); diff --git a/tests/Tests/ORM/Query/QueryTest.php b/tests/Tests/ORM/Query/QueryTest.php index d24d9292ca9..0336c3da6c8 100644 --- a/tests/Tests/ORM/Query/QueryTest.php +++ b/tests/Tests/ORM/Query/QueryTest.php @@ -15,7 +15,6 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Types; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Query\Parameter; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\UnitOfWork; @@ -40,8 +39,6 @@ class QueryTest extends OrmTestCase { - use VerifyDeprecations; - /** @var EntityManagerMock */ protected $entityManager; diff --git a/tests/Tests/ORM/QueryBuilderTest.php b/tests/Tests/ORM/QueryBuilderTest.php index 3c3f05ba4db..5e87ebe8126 100644 --- a/tests/Tests/ORM/QueryBuilderTest.php +++ b/tests/Tests/ORM/QueryBuilderTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM; +use BadMethodCallException; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Cache; @@ -80,6 +81,15 @@ public function testSimpleSelect(): void $this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + public function testSimpleSelectArray(): void + { + $qb = $this->entityManager->createQueryBuilder() + ->from(CmsUser::class, 'u') + ->select(['u.id', 'u.username']); + + $this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + public function testSimpleDelete(): void { $qb = $this->entityManager->createQueryBuilder() @@ -275,6 +285,18 @@ public function testWhere(): void $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :uid'); } + public function testWhereWithUnexpectedNamedArguments(): void + { + $qb = $this->entityManager->createQueryBuilder() + ->select('u') + ->from(CmsUser::class, 'u'); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Invalid call to Doctrine\ORM\QueryBuilder::where(), unknown named arguments: foo, bar'); + + $qb->where(foo: 'u.id = :uid', bar: 'u.name = :name'); + } + public function testComplexAndWhere(): void { $qb = $this->entityManager->createQueryBuilder() diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index 63e13c319b1..550b1cfe1c8 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -11,7 +11,6 @@ use Doctrine\DBAL\Driver\Statement; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\Exception\EntityIdentityCollisionException; use Doctrine\ORM\Mapping\ClassMetadata; @@ -46,8 +45,6 @@ */ class UnitOfWorkTest extends OrmTestCase { - use VerifyDeprecations; - /** * SUT */