Skip to content

Incompatibilities between lazy and initialized collections #11021

@boesing

Description

@boesing

Bug Report

Q A
BC Break no
Version 2.16.2

Summary

When using Criteria to match entities within a collection with a specific foreign key value of a relation entity, one has to use <relation>.id (for example, so targeting the referenced key in the target entity).
This works flawless when used with an initialized Collection as that uses matching on ArrayCollection which uses ClosureExpressionVisitor.

The ClosureExpressionVisitor does actually handle <relation>.id as it checks for . within the column name:
https://github.com/doctrine/collections/blob/bdba62b690650fd5b3ae29067e464a1270fb04b6/src/Expr/ClosureExpressionVisitor.php#L43-L48

When the Collection we do want to filter with matching is not initialized, the EntityPersister is used. That is usually one of:

  • ManyToManyPersister
  • OneToManyPersister

So both of those persisters are called with their loadCriteria method, which does not have this kind of . check in the column name.

Current behavior

Using referenced entity columns as criteria does only work for initialized collections while they lead to exceptions in lazy loaded criterias.

How to reproduce

<?php

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

enum PermissionEnum: string
{

    case CRUD = 'crud';
    case CRU = 'cru';

    case R = 'r';
    case CU = 'cu';
}

#[Entity]
class Permission
{

    #[Id]
    #[GeneratedValue]
    public int|null $id = null;

    public function __construct(
        #[Column(type: 'string', enumType: PermissionEnum::class)]
        public PermissionEnum $permission
    ) {
    }
}

#[Entity]
class Group
{

    #[Id]
    #[GeneratedValue]
    public int|null $id = null;

    #[OneToMany(targetEntity: Permission::class, fetch: 'EXTRA_LAZY')]
    public Collection&Selectable $permissions;

    public function __construct()
    {
        $this->permissions = new ArrayCollection();
    }
}

#[Entity]
class User
{

    #[Id]
    #[GeneratedValue]
    public int|null $id = null;

    #[OneToMany(targetEntity: Group::class, fetch: 'EXTRA_LAZY')]
    public Collection&Selectable $groups;

    public function __construct()
    {
        $this->groups = new ArrayCollection();
    }
}

/** @var EntityManagerInterface $entityManager */
$entityManager = null;

$user = $entityManager->find(User::class, 1);
assert($user !== null);

/**
 * Groups is now `LazyCriteriaCollection` since there is `EXTRA_LAZY` passed to the relation attribute and therefore
 * nothing is loaded
 */
$groups = $user->groups;


$criteria = Criteria::create();
$expression = Criteria::expr();

$criteria->andWhere($expression->eq('permission.name', PermissionEnum::CRUD->value));

/**
 * This works flawless until the `groups` collection is not initialized.
 *
 * But depending on the request flow of an application, *maybe* at some point *all* groups were already loaded due to
 * another section printing all group names of a specific user or whatever other reason, this will fail as the entity
 * persister is actually running into "unknown column permission.name for entity group" (just freely phrased, might be
 * a different text tho).
 */
$hasCrudPermission = !$groups->matching($criteria)->isEmpty();

Expected behavior

I would expect that criterias behave the same, especially since usually one is only annotating Collection&Selectable to avoid having concrete implementations handed over to userland code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions