Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Criteria with enums to filter lazy loaded collections #11481

Open
kira0269 opened this issue May 29, 2024 · 2 comments
Open

Criteria with enums to filter lazy loaded collections #11481

kira0269 opened this issue May 29, 2024 · 2 comments

Comments

@kira0269
Copy link

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.

@kira0269
Copy link
Author

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
Copy link
Member

stof commented Jun 20, 2024

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

kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 21, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 21, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 21, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 21, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 24, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Jun 24, 2024
kira0269 pushed a commit to kira0269/orm that referenced this issue Aug 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants