Skip to content

Sortable fails when there's SortableGroup in a child relation #3009

@kov-lucas

Description

@kov-lucas

Environment

  • php 8.4.13
  • Linux 88ac4bca2e3b 6.12.54-linuxkit #1 SMP Tue Nov 4 21:21:47 UTC 2025 aarch64
  • Symfony 7.3.4

Package

show

name     : gedmo/doctrine-extensions
descrip. : Doctrine behavioral extensions
keywords : Blameable, behaviors, doctrine, extensions, gedmo, loggable, nestedset, odm, orm, sluggable, sortable, timestampable, translatable, tree, uploadable
versions : * v3.21.0
released : 2025-09-22, 1 month ago
latest   : v3.21.0 released 2025-09-22, 1 month ago
type     : library
license  : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage : http://gediminasm.org/
source   : [git] https://github.com/doctrine-extensions/DoctrineExtensions.git eb53dfcb2b592327b76ac5226fbb003d32aea37e
dist     : [zip] https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eb53dfcb2b592327b76ac5226fbb003d32aea37e eb53dfcb2b592327b76ac5226fbb003d32aea37e
path     : /user/domains/Dev/site/vendor/gedmo/doctrine-extensions
names    : gedmo/doctrine-extensions

support
docs : https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc
issues : https://github.com/doctrine-extensions/DoctrineExtensions/issues
source : https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.21.0

autoload
psr-4
Gedmo\ => src/

requires
doctrine/collections ^1.2 || ^2.0
doctrine/deprecations ^1.0
doctrine/event-manager ^1.2 || ^2.0
doctrine/persistence ^2.2 || ^3.0 || ^4.0
php ^7.4 || ^8.0
psr/cache ^1 || ^2 || ^3
psr/clock ^1
symfony/cache ^5.4 || ^6.0 || ^7.0
symfony/string ^5.4 || ^6.0 || ^7.0

requires (dev)
behat/transliterator ^1.2
doctrine/annotations ^1.13 || ^2.0
doctrine/cache ^1.11 || ^2.0
doctrine/common ^2.13 || ^3.0
doctrine/dbal ^3.7 || ^4.0
doctrine/doctrine-bundle ^2.3
doctrine/mongodb-odm ^2.3
doctrine/orm ^2.20 || ^3.3
friendsofphp/php-cs-fixer ^3.70
nesbot/carbon ^2.71 || ^3.0
phpstan/phpstan ^2.1.1
phpstan/phpstan-doctrine ^2.0.1
phpstan/phpstan-phpunit ^2.0.3
phpunit/phpunit ^9.6
rector/rector ^2.0.6
symfony/console ^5.4 || ^6.0 || ^7.0
symfony/doctrine-bridge ^5.4 || ^6.0 || ^7.0
symfony/phpunit-bridge ^6.4 || ^7.0
symfony/uid ^5.4 || ^6.0 || ^7.0
symfony/yaml ^5.4 || ^6.0 || ^7.0

suggests
doctrine/mongodb-odm to use the extensions with the MongoDB ODM
doctrine/orm to use the extensions with the ORM

conflicts
behat/transliterator <1.2 || >=2.0
doctrine/annotations <1.13 || >=3.0
doctrine/common <2.13 || >=4.0
doctrine/dbal <3.7 || >=5.0
doctrine/mongodb-odm <2.3 || >=3.0
doctrine/orm <2.20 || >=3.0 <3.3 || >=4.0

Doctrine packages

show

Color legend:
- patch or minor release available - update recommended
- major release available - update possible
- up to date version

Direct dependencies required in composer.json:
doctrine/dbal                       3.9.3  4.3.4 Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.
doctrine/doctrine-bundle            2.18.0 3.0.0 Symfony DoctrineBundle
doctrine/doctrine-migrations-bundle 3.5.0  3.7.0 Symfony DoctrineMigrationsBundle
doctrine/orm                        3.5.2  3.5.7 Object-Relational-Mapper for PHP

Transitive dependencies not required in composer.json:
doctrine/cache                      2.2.0  2.2.0 PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.
Package doctrine/cache is abandoned, you should avoid using it. No replacement was suggested.
doctrine/collections                2.3.0  2.4.0 PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.
doctrine/deprecations               1.1.5  1.1.5 A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.
doctrine/event-manager              2.0.1  2.0.1 The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.
doctrine/inflector                  2.1.0  2.1.0 PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.
doctrine/instantiator               2.0.0  2.0.0 A small, lightweight utility to instantiate objects in PHP without invoking their constructors
doctrine/lexer                      3.0.1  3.0.1 PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.
doctrine/migrations                 3.9.4  3.9.5 PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily de...
doctrine/persistence                4.1.1  4.1.1 The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.
doctrine/sql-formatter              1.5.2  1.5.3 a PHP SQL highlighting library

PHP version

PHP 8.4.13 (cli) (built: Sep 30 2025 00:02:18) (NTS)
Copyright (c) The PHP Group
Built by https://github.com/docker-library/php
Zend Engine v4.4.13, Copyright (c) Zend Technologies
    with Zend OPcache v8.4.13, Copyright (c), by Zend Technologies

Subject

This is the error obtained, which happens at ORM::updatePositions due to the fact that all Gallery items are deleted, the gallery is deleted and also its parent. I believe there's a miscalculation, making the following code fail:

        foreach ($relocation['groups'] as $group => $value) {
            if (null === $value) {
                $dql .= " AND n.{$group} IS NULL";
            } else {
                $dql .= " AND n.{$group} = :val___".(++$i);
                $params['val___'.$i] = $value;
            }
        }

Since $value is not present; Gallery is there, but its id = null

        $em = $this->getObjectManager();
        $q = $em->createQuery($dql);
        $q->setParameters($params);
        $q->getSingleScalarResult();

giving this error:

Binding entities to query parameters only allowed for entities that have an identifier.
Class "App\Entity\Gallery" does not have an identifier.

That's why we suspect that even doctrine marking ParentA, its Gallery children and their Items deleted, Sortable is not keeping track of this, causing it to try to sort deleted items.

Minimal repository with the bug

Not sure

Steps to reproduce

Here's a reproducible case with all needed entities, migrations and a simple test case.

Entities

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class ParentA
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToOne(cascade: ['persist', 'remove'])]
    private ?Gallery $gallery = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getGallery(): ?Gallery
    {
        return $this->gallery;
    }

    public function setGallery(?Gallery $gallery): static
    {
        $this->gallery = $gallery;

        return $this;
    }
}

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Gallery
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    /**
     * @var Collection<int, Items>
     */
    #[ORM\OneToMany(targetEntity: Items::class, mappedBy: 'gallery', orphanRemoval: true)]
    private Collection $items;

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

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @return Collection<int, Items>
     */
    public function getItems(): Collection
    {
        return $this->items;
    }

    public function addItem(Items $item): static
    {
        if (!$this->items->contains($item)) {
            $this->items->add($item);
            $item->setGallery($this);
        }

        return $this;
    }

    public function removeItem(Items $item): static
    {
        if ($this->items->removeElement($item)) {
            // set the owning side to null (unless already changed)
            if ($item->getGallery() === $this) {
                $item->setGallery(null);
            }
        }

        return $this;
    }
}

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

#[ORM\Entity]
class Items
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(inversedBy: 'items')]
    #[ORM\JoinColumn(nullable: false)]
    #[Gedmo\SortableGroup]
    private ?Gallery $gallery = null;

    #[ORM\Column(options: ['default' => 0])]
    #[Gedmo\SortablePosition]
    private ?int $position = 0;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getGallery(): ?Gallery
    {
        return $this->gallery;
    }

    public function setGallery(?Gallery $gallery): static
    {
        $this->gallery = $gallery;

        return $this;
    }

    public function getPosition(): int
    {
        return $this->position;
    }

    public function setPosition(int $position): static
    {
        $this->position = $position;

        return $this;
    }
}

Migrations

        $this->addSql('CREATE TABLE gallery (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
        $this->addSql('CREATE TABLE items (id INT AUTO_INCREMENT NOT NULL, gallery_id INT NOT NULL, INDEX IDX_E11EE94D4E7AF8F (gallery_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
        $this->addSql('CREATE TABLE parent_a (id INT AUTO_INCREMENT NOT NULL, gallery_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_7C1C0DB64E7AF8F (gallery_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
        $this->addSql('ALTER TABLE items ADD CONSTRAINT FK_E11EE94D4E7AF8F FOREIGN KEY (gallery_id) REFERENCES gallery (id)');
        $this->addSql('ALTER TABLE parent_a ADD CONSTRAINT FK_7C1C0DB64E7AF8F FOREIGN KEY (gallery_id) REFERENCES gallery (id)');
        $this->addSql('ALTER TABLE items ADD position INT DEFAULT 0 NOT NULL');

        $this->addSql('INSERT INTO `gallery` (`id`) VALUES (\'1\')');
        $this->addSql('INSERT INTO `items` (`gallery_id`) VALUES (\'1\')');
        $this->addSql('INSERT INTO `items` (`gallery_id`) VALUES (\'1\')');
        $this->addSql('INSERT INTO `items` (`gallery_id`) VALUES (\'1\')');
        $this->addSql('INSERT INTO `parent_a` (`gallery_id`) VALUES (\'1\');');

Controller

<?php

declare(strict_types=1);

namespace App\Controller;

use Doctrine\ORM\EntityManagerInterface;
use App\Entity\ParentA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class TestController extends AbstractController
{
    public function __construct(
        private EntityManagerInterface $em
    ) {
    }

    public function __invoke(): Response
    {
        $entity = $this->em->getRepository(ParentA::class)->find(1);
        dump($entity);
        $this->em->remove($entity);
        $this->em->flush();
        dd($entity);
    }
}

Expected results

No error is shown and the sorting is properly done.

Actual results

Binding entities to query parameters only allowed for entities that have an identifier.
Class "App\Entity\Gallery" does not have an identifier.

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