Symfony bundle for automatic sort order management in Doctrine ORM entities. Recalculates sort positions on every EntityManager::flush(), keeping ordered lists consistent without manual reindexing. Uses PHP attributes to mark the sort field, supports grouped ordering (e.g., per parent or category), and handles insertions, deletions, and reordering transparently.
- Automatic reordering — sort positions are recalculated on flush, no manual gaps or renumbering needed
- Grouped sorting — maintain independent sort orders per parent, category, or any relation via
groupBy - PHP attribute configuration — single
#[Sort]attribute on your entity property, zero YAML/XML config - Drag-and-drop ready — set the new position and flush; surrounding items shift automatically
- Second-level cache support — works with Doctrine SLC; optionally evict cache collections and query regions on sort changes
- Change tracking policies — compatible with both
DEFERRED_IMPLICITandDEFERRED_EXPLICITtracking - Provided traits —
SortTraitandSortByParentTraitwith convenience methods (moveUp,moveDown,moveToBeginning,moveToEnd)
composer require chamber-orchestra/doctrine-sort-bundleIf Symfony Flex is enabled, the bundle is registered automatically. Otherwise, add it to config/bundles.php:
return [
// ...
ChamberOrchestra\DoctrineSortBundle\ChamberOrchestraDoctrineSortBundle::class => ['all' => true],
];Add the #[Sort] attribute to any integer column. The bundle will automatically maintain sequential ordering starting from 1.
use ChamberOrchestra\DoctrineSortBundle\Mapping\Attribute\Sort;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class TodoItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'integer', options: ['unsigned' => true, 'default' => 0])]
#[Sort]
private int $sortOrder = 0;
// Setting sortOrder to 0 appends the item to the end of the list.
// Setting sortOrder to 1 moves it to the beginning.
// Any value in between inserts at that position; surrounding items shift automatically.
}Maintain separate sort sequences per parent, category, or any relation:
use ChamberOrchestra\DoctrineSortBundle\Mapping\Attribute\Sort;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Task
{
#[ORM\ManyToOne]
private ?Project $project = null;
#[ORM\Column(type: 'integer', options: ['unsigned' => true, 'default' => 0])]
#[Sort(groupBy: ['project'])]
private int $sortOrder = 0;
// Each project has its own independent sort sequence.
// Moving a task to a different project removes it from the old sequence
// and inserts it into the new one.
}For common cases, use the provided traits instead of writing boilerplate:
use ChamberOrchestra\DoctrineSortBundle\Contracts\Entity\SortInterface;
use ChamberOrchestra\DoctrineSortBundle\Entity\SortTrait;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class TodoItem implements SortInterface
{
use SortTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
}SortTrait provides getSortOrder(), setSortOrder(), and convenience methods:
| Method | Effect |
|---|---|
moveUp() |
Decrease sort position by 1 (minimum 1) |
moveDown() |
Increase sort position by 1 |
moveToBeginning() |
Set sort position to 1 |
moveToEnd() |
Set sort position to 0 (appends to end) |
SortByParentTrait extends SortTrait with a $parent ManyToOne field and #[Sort(groupBy: ['parent'])].
#[Sort(
groupBy: ['parent'], // Fields that define independent sort groups
evictCollections: [ // Second-level cache collections to evict on change
[ParentEntity::class, 'children'],
],
evictRegions: ['my_query_region'], // Query cache regions to evict on change
)]| Option | Type | Default | Description |
|---|---|---|---|
groupBy |
array |
[] |
Entity field names that define sort groups. Each unique combination of group values has its own sort sequence. Supports Column, ManyToOne, and ManyToMany fields. |
evictCollections |
array |
[] |
List of [FQCN, collectionField] pairs. These Doctrine second-level cache collections are evicted when sort order changes. |
evictRegions |
array |
[] |
Query cache region names to evict when sort order changes. |
- The bundle listens to Doctrine's
onFlushevent - For each entity with a
#[Sort]attribute that was inserted, updated, or deleted, it collects the changes - Overlapping changes within the same group are merged into ranges to minimize database queries
- Affected entities are fetched, reordered in memory, and sort values are reassigned sequentially
- Doctrine's
recomputeSingleEntityChangeSet()ensures the corrected values are persisted in the same flush
This means you never need to manually manage gaps, shift items, or renumber sequences — just set the desired position and flush. Multiple reorder operations within a single flush or across multiple flushes in an explicit transaction are fully supported.
| Bundle | PHP | Symfony | Doctrine ORM |
|---|---|---|---|
| 8.0 | ^8.5 | 8.0 | 3.6+ |
composer testMIT