Criteria with enums to filter lazy loaded collections #7378

Open
opened 2026-01-22 15:50:54 +01:00 by admin · 3 comments
Owner

Originally created by @kira0269 on GitHub (May 29, 2024).

Bug Report

Q A
BC Break no
Version 3.2.0

Summary

Working with enums in matching criteria for collections does not work correctly for bot lazy and eager loading.

Current behavior

When filtering collections with $collection->matching($criteria), if the collection is not initialized, the values from the criteria object won't be converted to database types. So \BackedEnums are not replaced by their scalar value.

How to reproduce

  1. Setup the entities and the enum below:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;

class Page
{
    #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'page')]
    private Collection $comments;

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

    public function getComments(): Collection
    {
        return $this->comments;
    }
    
    public function getBanishedComments(): Collection
    {
        $criteria = Criteria::create()
            ->andWhere(Criteria::expr()->eq('commentStatus', CommentStatus::BANISHED));
        
        return $this->comments->matching($criteria);
    }
}

// ...

class Comment
{
    #[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'comments')]
    private Page $page;

    #[ORM\Column(nullable: false, enumType: CommentStatus::class)]
    private CommentStatus $commentStatus = CommentStatus::OK;

    public function getCommentStatus(): CommentStatus
    {
        return $this->commentStatus;
    }
} 

// ...

enum CommentStatus: string
{
    case OK = 'ok';
    case BANISHED = 'banished';
}
  1. Keep the default doctrine configuration. Below the config from my symfony application:
doctrine:
    dbal:
        default_connection: my_db
        connections:
            my_db:
                dbname: '%env(DB_NAME)%'
                host: '%env(DB_HOST)%'
                port: '%env(DB_PORT)%'
                user: '%env(DB_USER)%'
                password: '%env(DB_PASSWORD)%'
                driver: pdo_mysql
                server_version: '5.7.42'
                schema_filter: ~^(?!(logs)$)~ # Exclude logs table from doctrine management
                default_table_options:
                    collation: utf8mb4_unicode_ci
        types:
            tinyint: App\Doctrine\DBAL\Types\TinyintType
            mediumint: App\Doctrine\DBAL\Types\MediumintType
            
    orm:
        auto_generate_proxy_classes: true
        enable_lazy_ghost_objects: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        report_fields_where_declared: true
        auto_mapping: true
        controller_resolver:
            auto_mapping: true
        mappings:
            App:
                is_bundle: false
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App
  1. Add some records into your database
  2. Test the two cases:
/** @var EntityManagerInterface $em **/
/** @var Page $page **/
$page = $em->getRepository(PageRepository::class)->find(1);

// Case 1 : without initiliazed collection
$banishedComments = $page->getBanishedComments(); // ☠️ This will throw an exception and display the message "Object of class App\CommentStatus could not be converted to string"

// Case 1 : with initiliazed collection
$comments = $page->getComments(); 

// This loop will initialize the collection
foreach ($comments as $comment) {
    echo $comment->getCommentStatus()->value . "\n";
}

$banishedComments = $page->getBanishedComments(); // 🆗 This works

There is the stack trace:

Object of class App\CommentStatus could not be converted to string

  at vendor/doctrine/dbal/src/Driver/PDO/Statement.php:48
  at PDOStatement->bindValue(2, object(CommentStatus), 2)
     (vendor/doctrine/dbal/src/Driver/PDO/Statement.php:48)
  at Doctrine\DBAL\Driver\PDO\Statement->bindValue(2, object(CommentStatus), 2)
     (vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:35)
  at Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware->bindValue(2, object(CommentStatus), 2)
     (vendor/doctrine/dbal/src/Logging/Statement.php:84)
  at Doctrine\DBAL\Logging\Statement->bindValue(2, object(CommentStatus), 2)
     (vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:35)
  at Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware->bindValue(2, object(CommentStatus), 2)
     (vendor/symfony/doctrine-bridge/Middleware/Debug/DBAL3/Statement.php:54)
  at Symfony\Bridge\Doctrine\Middleware\Debug\DBAL3\Statement->bindValue(2, object(CommentStatus), 2)
     (vendor/doctrine/dbal/src/Connection.php:1809)
  at Doctrine\DBAL\Connection->bindParameters(object(Statement), array(1, object(CommentStatus)), array('integer', 'string'))
     (vendor/doctrine/dbal/src/Connection.php:1097)
  at Doctrine\DBAL\Connection->executeQuery('...')
     (vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php:273)
  at Doctrine\ORM\Persisters\Collection\ManyToManyPersister->loadCriteria(object(PersistentCollection), object(Criteria))
     (vendor/doctrine/orm/src/PersistentCollection.php:575)
  at Doctrine\ORM\PersistentCollection->matching(object(Criteria))
     (src/Entity/Page.php:118)
  at App\Entity\Page->getBanishedComments()
     (vendor/symfony/property-access/PropertyAccessor.php:388)
  at Symfony\Component\PropertyAccess\PropertyAccessor->readProperty(array(object(Page)), 'banishedComments', false)
     (vendor/symfony/property-access/PropertyAccessor.php:99)
  at Symfony\Component\PropertyAccess\PropertyAccessor->getValue(object(Page), 'banishedComments')
     (vendor/easycorp/easyadmin-bundle/src/Field/Configurator/CommonPreConfigurator.php:50)
  at EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CommonPreConfigurator->configure(object(FieldDto), object(EntityDto), object(AdminContext))
     (vendor/easycorp/easyadmin-bundle/src/Factory/FieldFactory.php:107)
  at EasyCorp\Bundle\EasyAdminBundle\Factory\FieldFactory->processFields(object(EntityDto), object(FieldCollection))
     (vendor/easycorp/easyadmin-bundle/src/Factory/EntityFactory.php:43)

Expected behavior

I expect the same behavior between the two cases.

Originally created by @kira0269 on GitHub (May 29, 2024). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 3.2.0 #### Summary Working with enums in matching criteria for collections does not work correctly for bot lazy and eager loading. #### Current behavior When filtering collections with `$collection->matching($criteria)`, if the collection is not initialized, the values from the criteria object won't be converted to database types. So `\BackedEnum`s are not replaced by their scalar value. #### How to reproduce 1. Setup the entities and the enum below: ```php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; class Page { #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'page')] private Collection $comments; public function __construct() { $this->comments = new ArrayCollection(); } public function getComments(): Collection { return $this->comments; } public function getBanishedComments(): Collection { $criteria = Criteria::create() ->andWhere(Criteria::expr()->eq('commentStatus', CommentStatus::BANISHED)); return $this->comments->matching($criteria); } } // ... class Comment { #[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'comments')] private Page $page; #[ORM\Column(nullable: false, enumType: CommentStatus::class)] private CommentStatus $commentStatus = CommentStatus::OK; public function getCommentStatus(): CommentStatus { return $this->commentStatus; } } // ... enum CommentStatus: string { case OK = 'ok'; case BANISHED = 'banished'; } ``` 2. Keep the default doctrine configuration. Below the config from my symfony application: ```yaml doctrine: dbal: default_connection: my_db connections: my_db: dbname: '%env(DB_NAME)%' host: '%env(DB_HOST)%' port: '%env(DB_PORT)%' user: '%env(DB_USER)%' password: '%env(DB_PASSWORD)%' driver: pdo_mysql server_version: '5.7.42' schema_filter: ~^(?!(logs)$)~ # Exclude logs table from doctrine management default_table_options: collation: utf8mb4_unicode_ci types: tinyint: App\Doctrine\DBAL\Types\TinyintType mediumint: App\Doctrine\DBAL\Types\MediumintType orm: auto_generate_proxy_classes: true enable_lazy_ghost_objects: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware report_fields_where_declared: true auto_mapping: true controller_resolver: auto_mapping: true mappings: App: is_bundle: false dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App ``` 3. Add some records into your database 4. Test the two cases: ```php /** @var EntityManagerInterface $em **/ /** @var Page $page **/ $page = $em->getRepository(PageRepository::class)->find(1); // Case 1 : without initiliazed collection $banishedComments = $page->getBanishedComments(); // ☠️ This will throw an exception and display the message "Object of class App\CommentStatus could not be converted to string" // Case 1 : with initiliazed collection $comments = $page->getComments(); // This loop will initialize the collection foreach ($comments as $comment) { echo $comment->getCommentStatus()->value . "\n"; } $banishedComments = $page->getBanishedComments(); // 🆗 This works ``` There is the stack trace: ``` Object of class App\CommentStatus could not be converted to string at vendor/doctrine/dbal/src/Driver/PDO/Statement.php:48 at PDOStatement->bindValue(2, object(CommentStatus), 2) (vendor/doctrine/dbal/src/Driver/PDO/Statement.php:48) at Doctrine\DBAL\Driver\PDO\Statement->bindValue(2, object(CommentStatus), 2) (vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:35) at Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware->bindValue(2, object(CommentStatus), 2) (vendor/doctrine/dbal/src/Logging/Statement.php:84) at Doctrine\DBAL\Logging\Statement->bindValue(2, object(CommentStatus), 2) (vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:35) at Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware->bindValue(2, object(CommentStatus), 2) (vendor/symfony/doctrine-bridge/Middleware/Debug/DBAL3/Statement.php:54) at Symfony\Bridge\Doctrine\Middleware\Debug\DBAL3\Statement->bindValue(2, object(CommentStatus), 2) (vendor/doctrine/dbal/src/Connection.php:1809) at Doctrine\DBAL\Connection->bindParameters(object(Statement), array(1, object(CommentStatus)), array('integer', 'string')) (vendor/doctrine/dbal/src/Connection.php:1097) at Doctrine\DBAL\Connection->executeQuery('...') (vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php:273) at Doctrine\ORM\Persisters\Collection\ManyToManyPersister->loadCriteria(object(PersistentCollection), object(Criteria)) (vendor/doctrine/orm/src/PersistentCollection.php:575) at Doctrine\ORM\PersistentCollection->matching(object(Criteria)) (src/Entity/Page.php:118) at App\Entity\Page->getBanishedComments() (vendor/symfony/property-access/PropertyAccessor.php:388) at Symfony\Component\PropertyAccess\PropertyAccessor->readProperty(array(object(Page)), 'banishedComments', false) (vendor/symfony/property-access/PropertyAccessor.php:99) at Symfony\Component\PropertyAccess\PropertyAccessor->getValue(object(Page), 'banishedComments') (vendor/easycorp/easyadmin-bundle/src/Field/Configurator/CommonPreConfigurator.php:50) at EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CommonPreConfigurator->configure(object(FieldDto), object(EntityDto), object(AdminContext)) (vendor/easycorp/easyadmin-bundle/src/Factory/FieldFactory.php:107) at EasyCorp\Bundle\EasyAdminBundle\Factory\FieldFactory->processFields(object(EntityDto), object(FieldCollection)) (vendor/easycorp/easyadmin-bundle/src/Factory/EntityFactory.php:43) ``` #### Expected behavior I expect the same behavior between the two cases.
Author
Owner

@kira0269 commented on GitHub (May 29, 2024):

I found a "workaround" in order to make it work: I set the fetch mode to 'EAGER'. This way, the collection is always initialized and the comparison with the enum works.

In my case, it's still acceptable since I don't have too many records to load.

@kira0269 commented on GitHub (May 29, 2024): I found a "workaround" in order to make it work: I set the fetch mode to 'EAGER'. This way, the collection is always initialized and the comparison with the enum works. In my case, it's still acceptable since I don't have too many records to load.
Author
Owner

@stof commented on GitHub (Jun 20, 2024):

For anyone wanting to help on that, the support of enum values need to be added in ManyToManyPersister::loadCriteria

@stof commented on GitHub (Jun 20, 2024): For anyone wanting to help on that, the support of enum values need to be added in `ManyToManyPersister::loadCriteria`
Author
Owner

@mpdude commented on GitHub (Mar 31, 2025):

Please try #11895

@mpdude commented on GitHub (Mar 31, 2025): Please try #11895
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7378