chamber-orchestra/doctrine-sort-bundle
Install the Bundle
Add to composer.json:
"require": {
"chamber-orchestra/doctrine-sort-bundle": "^1.0"
}
Run composer require chamber-orchestra/doctrine-sort-bundle.
Enable in Symfony
Register in config/bundles.php:
return [
// ...
ChamberOrchestra\SortBundle\SortBundle::class => ['all' => true],
];
Configure an Entity
Use the [Sortable] attribute on your Doctrine entity:
use ChamberOrchestra\SortBundle\Attribute\Sortable;
#[ORM\Entity]
#[Sortable(group: 'menu_items')] // Define a sort group
class MenuItem
{
#[ORM\Column(type: 'integer')]
private ?int $position = null;
// ...
}
First Use Case Reorder items via a form or API, then persist:
$menuItem->setPosition(5); // Manually set or use a drag-and-drop UI
$entityManager->flush(); // Bundle auto-recalculates positions in the group
Grouped Sorting
#[Sortable(group: '...')] are auto-sorted by position on flush().Playlist entity with #[Sortable(group: 'user_playlists')] will recalculate positions for all items in that group.Manual Position Updates
position directly (e.g., after drag-and-drop):
$item->setPosition($newPosition);
$entityManager->flush(); // Triggers recalculation
position gaps; let the bundle handle gaps on save.Second-Level Cache Integration
findByGroup()):
# config/packages/sort.yaml
chamber_orchestra_sort:
cache: true
cache_lifetime: 3600 # 1 hour
sort:menu_items).Querying Sorted Results Use the repository method:
$sortedItems = $menuItemRepo->findByGroup('menu_items', $direction = 'ASC');
Symfony Forms
Bind a position field to your form for manual input:
$builder->add('position', HiddenType::class);
Use JavaScript (e.g., SortableJS) for drag-and-drop UIs.
APIs Return sorted data directly:
return $this->getDoctrine()->getRepository(MenuItem::class)
->findByGroup('menu_items', 'ASC');
Migrations
Add position column if missing:
$this->addSql('ALTER TABLE menu_items ADD position INT');
Testing
Mock the SortEvent subscriber to test recalculation logic:
$this->entityManager->getEventManager()->addEventSubscriber(
new SortEventSubscriber($entityManager)
);
Missing position Column
Column 'position' not found on flush.Cache Stale Data
position changes.cache_lifetime to 0 for testing:
chamber_orchestra_sort:
cache: true
cache_lifetime: 0 # Disable cache
Group Collisions
group value but different classes.menu_items_admin) or override the getSortGroup() method in your entity.Performance with Large Groups
Enable SQL Logging
Add to config/packages/dev/doctrine.yaml:
doctrine:
dbal:
logging: true
Check for UPDATE queries on the position column during flush().
Event Subscriber Debugging
Dump the SortEvent payload to verify group/position logic:
public function onFlush(SortEvent $event): void
{
dump($event->getGroup(), $event->getEntities());
}
Custom Recalculation Logic
Override the default gap-filling algorithm by extending SortEventSubscriber:
use ChamberOrchestra\SortBundle\Event\SortEvent;
class CustomSortSubscriber extends SortEventSubscriber
{
public function onFlush(SortEvent $event): void
{
$this->recalculatePositions($event->getGroup(), $event->getEntities(), 'CUSTOM_LOGIC');
}
}
Register it in services.yaml:
services:
App\EventSubscriber\CustomSortSubscriber:
tags: [doctrine.event_subscriber]
Dynamic Group Names Use a method to generate group names dynamically:
#[Sortable(group: 'menu_items')]
class MenuItem
{
public function getSortGroup(): string
{
return 'menu_items_' . $this->getUserId();
}
}
Non-Integer Positions
Extend the bundle to support strings or UUIDs for position by modifying the Sortable attribute and subscriber logic.
Cache Provider
The bundle defaults to Symfony’s cache system. For custom providers (e.g., Redis), implement Psr\Cache\CacheItemPoolInterface and configure:
chamber_orchestra_sort:
cache_pool: app.redis_cache
Attribute Overrides
The #[Sortable] attribute cannot be overridden per environment. Use multiple entities or dynamic group names instead.
How can I help you explore Laravel packages today?