doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.86k stars 2.5k forks source link

Criteria with enums to filter lazy loaded collections #11481

Open kira0269 opened 4 weeks ago

kira0269 commented 4 weeks ago

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
  2. Add some records into your database
  3. 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. 
kira0269 commented 4 weeks ago

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.

stof commented 6 days ago

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