Compare commits

..

20 Commits
3.3.1 ... 3.3.2

Author SHA1 Message Date
Grégoire Paris
c9557c588b Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2025-02-04 20:43:15 +01:00
Grégoire Paris
19912de927 Merge pull request #11820 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.2.1
Bump doctrine/.github from 7.1.0 to 7.2.1
2025-02-04 20:17:01 +01:00
dependabot[bot]
737cca5b78 Bump doctrine/.github from 7.1.0 to 7.2.1
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.1.0 to 7.2.1.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.1.0...7.2.1)

---
updated-dependencies:
- dependency-name: doctrine/.github
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 18:23:54 +00:00
Grégoire Paris
aff82af7de Merge pull request #11811 from greg0ire/quoteIdentifier-depr
Address quoteIdentifier() deprecation
2025-01-28 23:17:29 +01:00
Grégoire Paris
9e999ea1ff Merge pull request #11792 from dbannik/11783-failure-with-indexed-relation
11783 failure with indexed relation
2025-01-28 15:32:25 +01:00
Bob van de Vijver
6755bb0c7b Fix Hydration when use ManyToMany[indexBy]
The bug related (#11694) and fixed mapping of sql column alias to field in entity (#11783) and
invalidate cache [cache/persisted/entity|cache/persisted/collection] when sql filter changes
2025-01-27 15:35:59 +03:00
Grégoire Paris
aa141bf001 Address quoteIdentifier() deprecation
We should be using quoteSingleIdentifier(), assuming we only ever pass
single identifiers here.

See https://github.com/doctrine/dbal/pull/6590
2025-01-26 12:01:14 +01:00
Grégoire Paris
cf39e00553 Merge pull request #11807 from greg0ire/display-deprecations
Display Doctrine deprecations when running PHPUnit
2025-01-25 11:09:40 +01:00
Grégoire Paris
27b47841be Display Doctrine deprecations when running PHPUnit
This will give a signal that there is work to be done without blocking
other contributions by failing the build.
2025-01-25 10:53:36 +01:00
Grégoire Paris
c2a49327a7 Merge pull request #11799 from HypeMC/enum-array-error
Fix invalid enum value in array of enums
2025-01-22 11:54:59 +01:00
Maxime COLIN
9bd7242376 Introduce testNotListedValueInEnumArray 2025-01-22 02:25:34 +01:00
pawel-slowik
fff085b63f Fix documentation for JoinColumn nullable (#11798)
Nullability is not inherited from the PHP type. The change that enabled
this feature was reversed in https://github.com/doctrine/orm/pull/8732.
2025-01-22 00:12:00 +01:00
Grégoire Paris
5ad5b11ae1 Merge pull request #11769 from HypeMC/fix-reportfieldswheredeclared
Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared`
2025-01-20 23:48:32 +01:00
Grégoire Paris
c12fd2cb94 Merge pull request #11793 from greg0ire/doctrine-common-support
Ignore deprecations from doctrine/common
2025-01-18 22:29:13 +01:00
Grégoire Paris
44d5d4a779 Ignore deprecations from doctrine/common
These new issues are caused by doctrine/common 3.5.0, released 2 weeks
ago.
2025-01-17 08:33:24 +01:00
Grégoire Paris
5a599233c9 Merge pull request #11791 from jonnyeom/patch-2
UPGRADE: Document QueryBuilder::setParameters() Type enforcement
2025-01-17 07:55:07 +01:00
Jonny Eom
596da353c2 UPGRADE: Document QueryBuilder::setParameters() Type enforcement 2025-01-16 17:14:20 +01:00
Jamie Purchase
68c87740aa Update working-with-objects.rst (#7553)
Spelling.
2025-01-02 00:25:25 +01:00
Florian Sylvain
55dc02c39f changed confusing negative wording (#11775) 2024-12-31 00:09:28 +01:00
HypeMC
4feaa470af Fix fields of transient classes being considered duplicate with reportFieldsWhereDeclared 2024-12-18 15:42:12 +01:00
30 changed files with 354 additions and 46 deletions

View File

@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.1"

View File

@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.1"

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.1"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}

View File

@@ -124,6 +124,36 @@ WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydra
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
## BC BREAK: Enforce ArrayCollection Type on `\Doctrine\ORM\QueryBuilder::setParameters(ArrayCollection $parameters)`
The argument $parameters can no longer be a key=>value array. Only ArrayCollection types are allowed.
### Before
```php
$qb = $em->createQueryBuilder()
->select('u')
->from('User', 'u')
->where('u.id = :user_id1 OR u.id = :user_id2')
->setParameter(array(
'user_id1' => 1,
'user_id2' => 2
));
```
### After
```php
$qb = $em->createQueryBuilder()
->select('u')
->from('User', 'u')
->where('u.id = :user_id1 OR u.id = :user_id2')
->setParameter(new ArrayCollection(array(
new Parameter('user_id1', 1),
new Parameter('user_id2', 2)
)));
```
## BC BREAK: `Doctrine\ORM\Persister\Entity\EntityPersister::executeInserts()` return type changed to `void`
Implementors should adapt to the new signature, and should call

View File

@@ -903,8 +903,7 @@ defaults to "id", just as in one-to-one or many-to-one mappings.
Additionally, when using typed properties with Doctrine 2.9 or newer
you can skip ``targetEntity`` in ``ManyToOne`` and ``OneToOne``
associations as they will be set based on type. Also ``nullable``
attribute on ``JoinColumn`` will be inherited from PHP type. So that:
associations as they will be set based on type. So that:
.. configuration-block::
@@ -931,7 +930,7 @@ Is essentially the same as following:
<?php
/** One Product has One Shipment. */
#[OneToOne(targetEntity: Shipment::class)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id', nullable: false)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id')]
private Shipment $shipment;
.. code-block:: annotation
@@ -940,7 +939,7 @@ Is essentially the same as following:
/**
* One Product has One Shipment.
* @OneToOne(targetEntity="Shipment")
* @JoinColumn(name="shipment_id", referencedColumnName="id", nullable=false)
* @JoinColumn(name="shipment_id", referencedColumnName="id")
*/
private Shipment $shipment;

View File

@@ -16,7 +16,7 @@ is common to multiple entity classes.
Mapped superclasses, just as regular, non-mapped classes, can
appear in the middle of an otherwise mapped inheritance hierarchy
(through Single Table Inheritance or Class Table Inheritance). They
are not query-able, and need not have an ``#[Id]`` property.
are not query-able, and do not require an ``#[Id]`` property.
No database table will be created for a mapped superclass itself,
only for entity classes inheriting from it. That implies that a

View File

@@ -166,7 +166,7 @@ your code. See the following code:
Traversing the object graph for parts that are lazy-loaded will
easily trigger lots of SQL queries and will perform badly if used
to heavily. Make sure to use DQL to fetch-join all the parts of the
too heavily. Make sure to use DQL to fetch-join all the parts of the
object-graph that you need as efficiently as possible.

View File

@@ -14,6 +14,7 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
failOnNotice="true"
@@ -67,5 +68,12 @@
<var name="privileged_db_port" value="3306"/>
-->
<env name="COLUMNS" value="120"/>
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
</php>
<source ignoreSuppressionOfDeprecations="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

View File

@@ -29,11 +29,14 @@ class CollectionCacheKey extends CacheKey
public readonly string $entityClass,
public readonly string $association,
array $ownerIdentifier,
string $filterHash = '',
) {
ksort($ownerIdentifier);
$this->ownerIdentifier = $ownerIdentifier;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
$filterHash = $filterHash === '' ? '' : '_' . $filterHash;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . $filterHash);
}
}

View File

@@ -18,6 +18,7 @@ use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\UnitOfWork;
use function array_values;
@@ -35,6 +36,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
protected array $queuedCache = [];
protected string $regionName;
protected FilterCollection $filters;
protected CollectionHydrator $hydrator;
protected CacheLogger|null $cacheLogger;
@@ -48,6 +50,10 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->region = $region;
$this->persister = $persister;
$this->association = $association;
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
@@ -135,7 +141,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
public function count(PersistentCollection $collection): int
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$entry = $this->region->get($key);
if ($entry !== null) {

View File

@@ -36,7 +36,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers
public function delete(PersistentCollection $collection): void
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$this->persister->delete($collection);
@@ -53,7 +53,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
// Invalidate non initialized collections OR ordered collection
if ($isDirty && ! $isInitialized || $this->association->isOrdered()) {

View File

@@ -61,7 +61,7 @@ class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
public function delete(PersistentCollection $collection): void
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
$this->persister->delete($collection);
@@ -88,7 +88,7 @@ class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
$this->persister->update($collection);
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
if ($lock === null) {

View File

@@ -24,10 +24,12 @@ use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
use function func_get_args;
use function serialize;
use function sha1;
@@ -43,6 +45,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
protected TimestampCacheKey $timestampKey;
protected EntityHydrator $hydrator;
protected Cache $cache;
protected FilterCollection $filters;
protected CacheLogger|null $cacheLogger = null;
protected string $regionName;
@@ -64,6 +67,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
$cacheFactory = $cacheConfig->getCacheFactory();
$this->cache = $em->getCache();
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
@@ -215,7 +219,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
? $this->persister->expandCriteriaParameters($criteria)
: $this->persister->expandParameters($criteria);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash());
}
/**
@@ -472,7 +476,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
@@ -503,7 +507,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
@@ -546,12 +550,15 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
/** @param array<string, mixed> $ownerId */
protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId): CollectionCacheKey
protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId, /* string $filterHash */): CollectionCacheKey
{
$filterHash = (string) (func_get_args()[2] ?? ''); // todo: move to argument in next major release
return new CollectionCacheKey(
$this->metadataFactory->getMetadataFor($association->sourceEntity)->rootEntityName,
$association->fieldName,
$ownerId,
$filterHash,
);
}
}

View File

@@ -17,6 +17,7 @@ use function array_search;
use function assert;
use function count;
use function in_array;
use function is_array;
use function key;
use function reset;
use function sprintf;
@@ -138,14 +139,21 @@ class SimpleObjectHydrator extends AbstractHydrator
}
if ($value !== null && isset($cacheKeyInfo['enumType'])) {
$originalValue = $value;
$originalValue = $currentValue = $value;
try {
$value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
if (! is_array($originalValue)) {
$value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
} else {
$value = [];
foreach ($originalValue as $i => $currentValue) {
$value[$i] = $this->buildEnum($currentValue, $cacheKeyInfo['enumType']);
}
}
} catch (ValueError $e) {
throw MappingException::invalidEnumValue(
$entityName,
$cacheKeyInfo['fieldName'],
(string) $originalValue,
(string) $currentValue,
$cacheKeyInfo['enumType'],
$e,
);

View File

@@ -24,7 +24,7 @@ class DefaultQuoteStrategy implements QuoteStrategy
public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($class->fieldMappings[$fieldName]->quoted)
? $platform->quoteIdentifier($class->fieldMappings[$fieldName]->columnName)
? $platform->quoteSingleIdentifier($class->fieldMappings[$fieldName]->columnName)
: $class->fieldMappings[$fieldName]->columnName;
}
@@ -42,7 +42,7 @@ class DefaultQuoteStrategy implements QuoteStrategy
}
return isset($class->table['quoted'])
? $platform->quoteIdentifier($tableName)
? $platform->quoteSingleIdentifier($tableName)
: $tableName;
}
@@ -52,14 +52,14 @@ class DefaultQuoteStrategy implements QuoteStrategy
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($definition['quoted'])
? $platform->quoteIdentifier($definition['sequenceName'])
? $platform->quoteSingleIdentifier($definition['sequenceName'])
: $definition['sequenceName'];
}
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($joinColumn->quoted)
? $platform->quoteIdentifier($joinColumn->name)
? $platform->quoteSingleIdentifier($joinColumn->name)
: $joinColumn->name;
}
@@ -69,7 +69,7 @@ class DefaultQuoteStrategy implements QuoteStrategy
AbstractPlatform $platform,
): string {
return isset($joinColumn->quoted)
? $platform->quoteIdentifier($joinColumn->referencedColumnName)
? $platform->quoteSingleIdentifier($joinColumn->referencedColumnName)
: $joinColumn->referencedColumnName;
}
@@ -87,7 +87,7 @@ class DefaultQuoteStrategy implements QuoteStrategy
$tableName = $association->joinTable->name;
if (isset($association->joinTable->quoted)) {
$tableName = $platform->quoteIdentifier($tableName);
$tableName = $platform->quoteSingleIdentifier($tableName);
}
return $schema . $tableName;
@@ -113,7 +113,7 @@ class DefaultQuoteStrategy implements QuoteStrategy
$joinColumns = $assoc->joinColumns;
$assocQuotedColumnNames = array_map(
static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted)
? $platform->quoteIdentifier($joinColumn->name)
? $platform->quoteSingleIdentifier($joinColumn->name)
: $joinColumn->name,
$joinColumns,
);

View File

@@ -22,8 +22,13 @@ trait ReflectionBasedDriver
*/
private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
{
/** @var class-string $declaringClass */
$declaringClass = $property->class;
if ($this->isTransient($declaringClass)) {
return isset($metadata->fieldMappings[$property->name]);
}
if (
isset($metadata->fieldMappings[$property->name]->declared)
&& $metadata->fieldMappings[$property->name]->declared === $declaringClass

View File

@@ -1500,7 +1500,15 @@ class BasicEntityPersister implements EntityPersister
$tableAlias = $this->getSQLTableAlias($class->name, $root);
$fieldMapping = $class->fieldMappings[$field];
$sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
$columnAlias = null;
if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) {
$columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field);
}
if ($columnAlias === null) {
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
}
$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
if (! empty($fieldMapping->enumType)) {

View File

@@ -66,6 +66,13 @@ class ResultSetMapping
*/
public array $fieldMappings = [];
/**
* Map field names for each class to alias
*
* @var array<class-string, array<string, array<string, string>>>
*/
public array $columnAliasMappings = [];
/**
* Maps column names in the result set to the alias/field name to use in the mapped result.
*
@@ -328,7 +335,10 @@ class ResultSetMapping
// column name => alias of owner
$this->columnOwnerMap[$columnName] = $alias;
// field name => class name of declaring class
$this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias];
$declaringClass = $declaringClass ?: $this->aliasMap[$alias];
$this->declaringClasses[$columnName] = $declaringClass;
$this->columnAliasMappings[$declaringClass][$alias][$fieldName] = $columnName;
if (! $this->isMixed && $this->scalarMappings) {
$this->isMixed = true;
@@ -337,6 +347,20 @@ class ResultSetMapping
return $this;
}
public function hasColumnAliasByField(string $alias, string $fieldName): bool
{
$declaringClass = $this->aliasMap[$alias];
return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]);
}
public function getColumnAliasByField(string $alias, string $fieldName): string
{
$declaringClass = $this->aliasMap[$alias];
return $this->columnAliasMappings[$declaringClass][$alias][$fieldName];
}
/**
* Adds a joined entity result.
*

View File

@@ -12,6 +12,7 @@ use Doctrine\ORM\Query\AST\Literal;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\WhereClause;
use Doctrine\ORM\Query\SqlOutputWalker;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\ORM\Tools\Pagination\Paginator;
@@ -643,7 +644,7 @@ class PaginationTest extends OrmFunctionalTestCase
self::assertCount(2, $getCountQuery->invoke($paginator)->getParameters());
self::assertCount(9, $paginator);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, SqlWalker::class);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, SqlOutputWalker::class);
$paginator = new Paginator($query);

View File

@@ -14,6 +14,7 @@ use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionMethod;
use Symfony\Component\VarExporter\Instantiator;
use Symfony\Component\VarExporter\VarExporter;
@@ -34,6 +35,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
/** @param Closure(ParserResult): ParserResult $toSerializedAndBack */
#[DataProvider('provideToSerializedAndBack')]
#[WithoutErrorHandler]
public function testSerializeParserResultForQueryWithSqlWalker(Closure $toSerializedAndBack): void
{
$query = $this->_em

View File

@@ -42,14 +42,14 @@ class DDC832Test extends OrmFunctionalTestCase
$platform = $this->_em->getConnection()->getDatabasePlatform();
$sm = $this->createSchemaManager();
$sm->dropTable($platform->quoteIdentifier('TREE_INDEX'));
$sm->dropTable($platform->quoteIdentifier('INDEX'));
$sm->dropTable($platform->quoteIdentifier('LIKE'));
$sm->dropTable($platform->quoteSingleIdentifier('TREE_INDEX'));
$sm->dropTable($platform->quoteSingleIdentifier('INDEX'));
$sm->dropTable($platform->quoteSingleIdentifier('LIKE'));
// DBAL 3
if ($platform instanceof PostgreSQLPlatform && method_exists($platform, 'getIdentitySequenceName')) {
$sm->dropSequence($platform->quoteIdentifier('INDEX_id_seq'));
$sm->dropSequence($platform->quoteIdentifier('LIKE_id_seq'));
$sm->dropSequence($platform->quoteSingleIdentifier('INDEX_id_seq'));
$sm->dropSequence($platform->quoteSingleIdentifier('LIKE_id_seq'));
}
}

View File

@@ -30,6 +30,15 @@ class GH10450Test extends OrmTestCase
yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10450EntityChildProtected::class];
yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10450MappedSuperclassChildProtected::class];
}
public function testFieldsOfTransientClassesAreNotConsideredDuplicate(): void
{
$em = $this->getTestEntityManager();
$metadata = $em->getClassMetadata(GH10450Cat::class);
self::assertArrayHasKey('id', $metadata->fieldMappings);
}
}
#[ORM\Entity]
@@ -113,3 +122,26 @@ class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassP
#[ORM\Column(type: 'text', name: 'child')]
protected string $field;
}
abstract class GH10450AbstractEntity
{
#[ORM\Column(type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
protected int $id;
}
#[ORM\Entity]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap(['cat' => GH10450Cat::class])]
#[ORM\DiscriminatorColumn(name: 'type')]
abstract class GH10450Animal extends GH10450AbstractEntity
{
#[ORM\Column(type: 'text', name: 'base')]
private string $field;
}
#[ORM\Entity]
class GH10450Cat extends GH10450Animal
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'Category_Master')]
class Category
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
#[ORM\Column(type: 'string')]
public string $type;
public function __construct(string $name, string $type)
{
$this->name = $name;
$this->type = $type;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use function sprintf;
class CategoryTypeSQLFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if ($targetEntity->getName() === Category::class) {
return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['type']->fieldName, $this->getParameter('type'));
}
return '';
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\Tests\OrmFunctionalTestCase;
final class ChangeFiltersTest extends OrmFunctionalTestCase
{
private const COMPANY_A = 'A';
private const CAT_BAR = 'bar';
private const CAT_FOO = 'foo';
public function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
Company::class,
Category::class,
]);
}
private function prepareData(): void
{
$cat1 = new Category('cat1', self::CAT_FOO);
$cat2 = new Category('cat2', self::CAT_BAR);
$companyA = new Company(self::COMPANY_A, [$cat1, $cat2]);
$this->_em->persist($cat1);
$this->_em->persist($cat2);
$this->_em->persist($companyA);
$this->_em->flush();
$this->_em->clear();
}
public function testIndexAliasUpdatedWithUpdatedFilter(): void
{
$this->prepareData();
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
self::assertCount(2, $company->categories);
self::assertEquals([self::CAT_FOO, self::CAT_BAR], $company->categories->map(static function (Category $c): string {
return $c->type;
})->getValues());
$this->_em->clear();
$this->_em->getConfiguration()->addFilter(CategoryTypeSQLFilter::class, CategoryTypeSQLFilter::class);
$this->_em->getFilters()->enable(CategoryTypeSQLFilter::class)->setParameter('type', self::CAT_FOO);
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
self::assertCount(1, $company->categories);
self::assertEquals([self::CAT_FOO], $company->categories->map(static function (Category $c): string {
return $c->type;
})->getValues());
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'Company_Master')]
class Company
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
/** @var Collection<int, Category> */
#[ORM\ManyToMany(targetEntity: Category::class, fetch: 'EAGER', indexBy: 'type')]
public Collection $categories;
/** @param Category[] $categories */
public function __construct(string $name, array $categories)
{
$this->name = $name;
$this->categories = new ArrayCollection($categories);
}
}

View File

@@ -7,11 +7,15 @@ namespace Doctrine\Tests\ORM\Hydration;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\ORM\Internal\Hydration\HydrationException;
use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\DbalTypes\GH8565EmployeePayloadType;
use Doctrine\Tests\DbalTypes\GH8565ManagerPayloadType;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\Company\CompanyPerson;
use Doctrine\Tests\Models\Enums\Scale;
use Doctrine\Tests\Models\Enums\Unit;
use Doctrine\Tests\Models\GH8565\GH8565Employee;
use Doctrine\Tests\Models\GH8565\GH8565Manager;
use Doctrine\Tests\Models\GH8565\GH8565Person;
@@ -155,4 +159,25 @@ class SimpleObjectHydratorTest extends HydrationTestCase
$result = $hydrator->hydrateAll($stmt, $rsm);
self::assertEquals($result[0], $expectedEntity);
}
public function testNotListedValueInEnumArray(): void
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage('Case "unknown_case" is not listed in enum "Doctrine\Tests\Models\Enums\Unit"');
$rsm = new ResultSetMapping();
$rsm->addEntityResult(Scale::class, 's');
$rsm->addFieldResult('s', 's__id', 'id');
$rsm->addFieldResult('s', 's__supported_units', 'supportedUnits');
$rsm->addEnumResult('s__supported_units', Unit::class);
$resultSet = [
[
's__id' => 1,
's__supported_units' => 'g,m,unknown_case',
],
];
$stmt = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$hydrator = new SimpleObjectHydrator($this->entityManager);
$hydrator->hydrateAll($stmt, $rsm);
}
}

View File

@@ -52,6 +52,7 @@ use Doctrine\Tests\OrmTestCase;
use DoctrineGlobalArticle;
use LogicException;
use PHPUnit\Framework\Attributes\Group as TestGroup;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionClass;
use stdClass;
@@ -1074,6 +1075,7 @@ class ClassMetadataTest extends OrmTestCase
$metadata->addLifecycleCallback('foo', 'bar');
}
#[WithoutErrorHandler]
public function testGettingAnFQCNForNullIsDeprecated(): void
{
$metadata = new ClassMetadata(self::class);
@@ -1112,6 +1114,7 @@ class ClassMetadataTest extends OrmTestCase
);
}
#[WithoutErrorHandler]
public function testDiscriminatorMapWithSameClassMultipleTimesDeprecated(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/3519');

View File

@@ -6,12 +6,14 @@ namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Mapping\Table;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
final class TableMappingTest extends TestCase
{
use VerifyDeprecations;
#[WithoutErrorHandler]
public function testDeprecationOnIndexesPropertyIsTriggered(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357');
@@ -19,6 +21,7 @@ final class TableMappingTest extends TestCase
new Table(indexes: []);
}
#[WithoutErrorHandler]
public function testDeprecationOnUniqueConstraintsPropertyIsTriggered(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357');

View File

@@ -614,7 +614,7 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
}
if (isset($this->_usedModelSets['directorytree'])) {
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('file'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('file'));
// MySQL doesn't know deferred deletions therefore only executing the second query gives errors.
$conn->executeStatement('DELETE FROM Directory WHERE parentDirectory_id IS NOT NULL');
$conn->executeStatement('DELETE FROM Directory');
@@ -707,17 +707,17 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
$conn->executeStatement(
sprintf(
'UPDATE %s SET %s = NULL',
$platform->quoteIdentifier('quote-address'),
$platform->quoteIdentifier('user-id'),
$platform->quoteSingleIdentifier('quote-address'),
$platform->quoteSingleIdentifier('user-id'),
),
);
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-users-groups'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-group'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-phone'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-user'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-address'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-city'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-users-groups'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-group'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-phone'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-user'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-address'));
$conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-city'));
}
if (isset($this->_usedModelSets['vct_onetoone'])) {