Sorters are specifications that define how data can be ordered and sorted. They carry a sorting direction and allow users to control the order in which results are returned.
Sorters define the available sorting options for your data. For sorters that can apply direction, you can pass one of these values:
1
, '1'
, 'asc'
, SORT_ASC
for ascending order-1
, '-1'
, 'desc'
, SORT_DESC
for descending orderThe most common sorter that can sort by one or more fields in either direction:
use Spiral\DataGrid\Specification\Sorter\Sorter;
// Single field sorter
$schema->addSorter('name', new Sorter('name'));
// Multiple field sorter - sorts by first_name, then last_name
$schema->addSorter('full_name', new Sorter('first_name', 'last_name'));
// Usage: ?sort[name]=asc or ?sort[name]=desc
// Usage: ?sort[full_name]=desc
AscSorter
and DescSorter
contain expressions that are applied with a fixed sorting order:
use Spiral\DataGrid\Specification\Sorter\{AscSorter, DescSorter};
// Always sorts in ascending order
$ascSorter = new AscSorter('first_name', 'last_name');
// Always sorts in descending order
$descSorter = new DescSorter('created_at', 'priority');
These are useful when you want to enforce a specific sort order regardless of user input.
This sorter contains 2 independent sorters, each for ascending and descending order. This allows you to use different fields or logic for each direction:
use Spiral\DataGrid\Specification\Sorter\DirectionalSorter;
$sorter = new DirectionalSorter(
new AscSorter('first_name'), // Ascending: sort by first name
new DescSorter('last_name') // Descending: sort by last name
);
// User requests ascending sort
$ascSorter = $sorter->withDirection('asc'); // Sorts by first_name ASC
// User requests descending sort
$descSorter = $sorter->withDirection('desc'); // Sorts by last_name DESC
Note:
You can sort using different fields in both sorters. If you have the same fields for both directions, use a regular Sorter instead.
This combines multiple sorters into one set. When a direction is applied, it affects the entire set:
use Spiral\DataGrid\Specification\Sorter\{SorterSet, AscSorter, DescSorter, Sorter};
$sorter = new SorterSet(
new AscSorter('priority'), // Always ascending
new DescSorter('created_at'), // Always descending
new Sorter('name', 'email'), // Follows set direction
);
// When user sorts ascending:
// ORDER BY priority ASC, created_at DESC, name ASC, email ASC
$ascSorter = $sorter->withDirection('asc');
// When user sorts descending:
// ORDER BY priority ASC, created_at DESC, name DESC, email DESC
$descSorter = $sorter->withDirection('desc');
class ProductSchema extends GridSchema
{
public function __construct()
{
// Simple field sorting
$this->addSorter('name', new Sorter('name'));
$this->addSorter('price', new Sorter('price'));
$this->addSorter('created_at', new Sorter('created_at'));
// Complex popularity sorting
$this->addSorter('popularity', new SorterSet(
new DescSorter('featured'), // Featured products first
new DescSorter('sales_count'), // Then by sales
new DescSorter('rating'), // Then by rating
new AscSorter('price') // Finally by price
));
// Different logic for best/worst sellers
$this->addSorter('sales', new DirectionalSorter(
new DescSorter('sales_count', 'rating'), // Best sellers
new AscSorter('sales_count', 'created_at') // Worst sellers
));
}
}
class UserSchema extends GridSchema
{
public function __construct()
{
// Basic user sorting
$this->addSorter('name', new Sorter('first_name', 'last_name'));
$this->addSorter('email', new Sorter('email'));
$this->addSorter('joined', new Sorter('created_at'));
// Activity-based sorting
$this->addSorter('activity', new DirectionalSorter(
// Most active: recent login, high post count
new DescSorter('last_login', 'post_count'),
// Least active: old login, low post count
new AscSorter('last_login', 'post_count')
));
// Status-based sorting with multiple criteria
$this->addSorter('status', new SorterSet(
new DescSorter('is_premium'), // Premium users first
new AscSorter('status'), // Then by status
new DescSorter('created_at') // Then by registration date
));
}
}
Sorters accept input through URL query parameters:
GET /api/users?sort[name]=asc&sort[created_at]=desc
You can apply multiple sorts simultaneously:
GET /api/products?sort[category]=asc&sort[price]=desc&sort[name]=asc
This would produce SQL like:
ORDER BY category ASC, price DESC, name ASC
You can set default sorting in your controller:
public function products(ProductSchema $schema, GridFactoryInterface $factory, ProductRepository $products)
{
$grid = $factory->create($products->select(), $schema);
// Apply default sorting if none provided
if (!$this->request->has('sort')) {
$grid = $grid->withSort('popularity', 'desc');
}
return ['products' => iterator_to_array($grid)];
}
Different sorting behavior based on context:
class ProductSchema extends GridSchema
{
public function __construct(User $user = null)
{
// Basic sorting available to everyone
$this->addSorter('name', new Sorter('name'));
$this->addSorter('price', new Sorter('price'));
// Premium users get advanced sorting
if ($user && $user->isPremium()) {
$this->addSorter('profit_margin', new Sorter('profit_margin'));
$this->addSorter('stock_level', new Sorter('stock_quantity'));
}
// Admin-only sorting
if ($user && $user->isAdmin()) {
$this->addSorter('internal_priority', new Sorter('admin_priority'));
}
}
}
class SearchResultSchema extends GridSchema
{
public function __construct(string $searchQuery = '')
{
$this->addSorter('name', new Sorter('name'));
$this->addSorter('created_at', new Sorter('created_at'));
// If there's a search query, relevance sorting is available
if (!empty($searchQuery)) {
$this->addSorter('relevance', new DescSorter('search_rank'));
}
// Default to relevance when searching, created_at otherwise
$defaultSort = !empty($searchQuery) ? 'relevance' : 'created_at';
$this->addSorter('default', new Sorter($defaultSort));
}
}
For complex sorting requirements, you might need custom database functions:
// In your repository or query builder
class ProductRepository
{
public function select(): SelectQuery
{
return $this->database
->select()
->from('products')
->columns([
'*',
// Custom calculated fields for sorting
'((featured * 10) + (rating * 2) + sales_count) as popularity_score',
'CASE WHEN stock_quantity > 0 THEN 1 ELSE 0 END as in_stock'
]);
}
}
// Then in your schema
$this->addSorter('popularity', new Sorter('popularity_score'));
$this->addSorter('availability', new DescSorter('in_stock', 'stock_quantity'));
Ensure sorted fields have appropriate indexes:
// These sorters should have corresponding database indexes
$this->addSorter('created_at', new Sorter('created_at')); // INDEX(created_at)
$this->addSorter('status', new Sorter('status')); // INDEX(status)
$this->addSorter('user_id', new Sorter('user_id')); // INDEX(user_id)
// Composite sorting needs composite indexes
$this->addSorter('user_date', new Sorter('user_id', 'created_at')); // INDEX(user_id, created_at)
Be careful with expensive sorting operations:
// Efficient: Sorting by indexed columns
$this->addSorter('price', new Sorter('price'));
// Less efficient: Sorting by calculated values
$this->addSorter('total_value', new Sorter('price * quantity'));
// Better: Pre-calculate expensive values
$this->addSorter('total_value', new Sorter('calculated_total_value')); // Pre-computed column
Invalid sort directions are automatically handled:
// Schema defines valid sorter
$schema->addSorter('name', new Sorter('name'));
// Invalid input is ignored
// ?sort[name]=invalid_direction
// Result: No sorting applied, no error thrown
// Valid input is processed
// ?sort[name]=asc → ORDER BY name ASC
// ?sort[name]=desc → ORDER BY name DESC
class BlogPostSchema extends GridSchema
{
public function __construct()
{
// User-friendly sorter names
$this->addSorter('newest', new DescSorter('published_at'));
$this->addSorter('oldest', new AscSorter('published_at'));
$this->addSorter('popular', new DescSorter('view_count', 'like_count'));
$this->addSorter('title', new Sorter('title'));
$this->addSorter('author', new Sorter('author_name'));
// Performance-optimized sorting
$this->addSorter('trending', new DescSorter('trending_score')); // Pre-calculated
}
}