Installation
composer require cannibal/sort-bundle
Add to config/app.php under providers:
Cannibal\SortBundle\SortBundle::class,
Basic Usage
Enable sorting for a controller method by annotating the action with @Sortable:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Cannibal\SortBundle\Annotation\Sortable;
class ProductController extends Controller
{
/**
* @Route("/products", name="products_list")
* @Sortable
*/
public function listAction()
{
$products = $this->getDoctrine()->getRepository(Product::class)->findAll();
return $this->renderProducts($products);
}
}
First Use Case
?sort=price&direction=desc to the URI to sort products by price in descending order.Doctrine QueryBuilder Integration
Use @Sortable with a QueryBuilder to enable database-level sorting:
public function listAction()
{
$qb = $this->getDoctrine()->getRepository(Product::class)->createQueryBuilder('p');
return $this->renderProducts($qb->getQuery()->getResult());
}
The bundle will inject ORDER BY clauses dynamically.
Array Collections
For non-DB collections (e.g., API responses), use the SortService directly:
$sorted = $this->get('cannibal_sort.sort_service')->sort($collection, 'price', 'desc');
Field Mappings
Override default field names via configuration (config/packages/cannibal_sort.yaml):
cannibal_sort:
fields:
product: [id, name, price, 'created_at:date']
Here, created_at:date maps to a custom DateTime formatter.
Custom Sort Handlers
Implement Cannibal\SortBundle\Sort\SortHandlerInterface for complex logic:
class CustomSortHandler implements SortHandlerInterface
{
public function sort($collection, $field, $direction)
{
// Custom logic (e.g., multi-field sorting)
return usort($collection, fn($a, $b) => $a->$field <=> $b->$field);
}
}
Register it in services.yaml:
services:
App\Sort\CustomSortHandler:
tags: ['cannibal_sort.handler']
Request-Based Sorting
Let clients control sorting via query params (e.g., /products?sort[]=price&sort[]=-name).
The bundle supports multi-field sorting out of the box.
Response Formatting
Combine with Symfony\Serializer to return sorted, normalized data:
return $this->json($this->get('serializer')->serialize($sortedProducts, 'json'));
SortType field for form-based sorting preferences:
$builder->add('sort', SortType::class, [
'fields' => ['price', 'name'],
'default_direction' => 'asc',
]);
Doctrine DQL Support
Ensure your ORDER BY fields are valid DQL identifiers. Avoid raw SQL or HAVING clauses.
Pagination Compatibility
Pair with KnpPaginatorBundle or Symfony\Component\Pagination:
$paginator = $this->get('knp_paginator');
$results = $paginator->paginate(
$sortedProducts,
$page,
10
);
Caching Cache sorted results if the underlying data changes infrequently:
$cacheKey = 'products_sorted_' . md5(serialize($sortParams));
return $this->get('cache')->get($cacheKey, function() use ($qb) {
return $qb->getQuery()->getResult();
});
Testing
Mock the SortService in PHPUnit:
$mockSortService = $this->createMock(SortService::class);
$mockSortService->method('sort')->willReturn($sortedData);
$container->set('cannibal_sort.sort_service', $mockSortService);
Field Name Mismatches
created_at vs. createdAt).fields config to alias properties or ensure strict naming consistency.Case Sensitivity
"Apple" vs. "apple").SortHandler:
usort($collection, fn($a, $b) => strcasecmp($a->$field, $b->$field));
Nested Objects
user.name) requires custom handlers.NestedSortHandler:
class NestedSortHandler implements SortHandlerInterface
{
public function sort($collection, $field, $direction)
{
[$object, $property] = explode('.', $field);
usort($collection, fn($a, $b) => $a->$object->$property <=> $b->$object->$property);
}
}
Performance
usort with a custom comparator for lightweight arrays.Annotation Overhead
@Sortable adds complexity to controller methods.abstract class SortableController extends Controller
{
use SortableTrait;
}
Enable Verbose Logging
Add to config/packages/monolog.yaml:
handlers:
cannibal_sort:
type: stream
path: "%kernel.logs_dir%/sort.log"
level: debug
channels: ["cannibal_sort"]
Then log sort operations in your SortHandler:
$this->logger->debug('Sorting by', ['field' => $field, 'direction' => $direction]);
Check Request Params
Dump the sort and direction params in your controller to verify input:
dump($this->get('request')->query->all());
Validate Field Existence
Ensure fields exist on the object to avoid Undefined property errors:
if (!property_exists($item, $field)) {
throw new \InvalidArgumentException("Field '$field' not found.");
}
Custom Sort Directions
Extend the direction parameter to support custom logic (e.g., natural, reverse):
if ($direction === 'natural') {
usort($collection, 'strnatcasecmp');
}
Dynamic Field Whitelisting Restrict sortable fields per entity via a service:
$this->get('cannibal_sort.field_whitelist')->add('Product', ['price', 'name']);
Event Listeners Trigger events before/after sorting:
# config/packages/cannibal_sort.yaml
cannibal_sort:
events:
pre_sort: App\EventListener\PreSortListener
post_sort: App\EventListener\PostSortListener
Localization Support
Add locale-aware sorting for strings:
use Symfony\Component\Intl\Locales;
usort($collection, fn($a, $b) =>
strcoll($a->$field, $b->$field, $this->getParameter('locale'))
);
Batch Processing For huge datasets, implement chunked sorting:
$batchSize = 1000;
$sorted = [];
foreach (array_chunk($collection, $batchSize) as $chunk) {
$sorted = array_merge($sorted, $this->sortChunk($chunk, $field, $direction));
}
How can I help you explore Laravel packages today?