-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Paginator sortMap. #18898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 5.next
Are you sure you want to change the base?
Paginator sortMap. #18898
Conversation
Looks good, should we handle what i mentioned here: #18897 (comment) (sharing the same field name on multiple sortmaps) |
I don't think we need validation to prevent "overlapping" fields since each mapped key represents a complete, independent sorting strategy. This is actually a feature - it lets you create multiple sort options that include The concern about ambiguity is resolved by the design:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a new sortMap
feature to the NumericPaginator class that allows creating friendly sort keys that map to one or more actual database fields. The implementation provides advanced sorting capabilities including simple 1:1 mappings, multi-column sorting, fixed direction sorting, and shorthand syntax support.
Key changes include:
- Added
sortMap
configuration option to allow mapping user-friendly sort keys to database fields - Implemented support for multiple sorting patterns including simple mapping, multi-column sorting, and fixed directions
- Enhanced test coverage with comprehensive test cases for all sortMap functionality
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
File | Description |
---|---|
src/Datasource/Paging/NumericPaginator.php | Added sortMap configuration and resolveSortMapping method to handle mapped sorting logic |
tests/TestCase/Datasource/Paging/NumericPaginatorTest.php | Added comprehensive test cases covering all sortMap functionality scenarios |
tests/TestCase/Datasource/Paging/PaginatorTestTrait.php | Updated test expectations to include sortMap null default in merged options |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
// Check sortMap first for mapped sorting | ||
if (isset($options['sortMap'])) { | ||
$mappedOrder = $this->resolveSortMapping($options['sort'], $options['sortMap'], $direction); | ||
if ($mappedOrder !== null) { | ||
// Use mapped order and merge with existing order | ||
$existingOrder = isset($options['order']) && is_array($options['order']) ? $options['order'] : []; | ||
$options['order'] = $mappedOrder + $existingOrder; | ||
} else { | ||
// Sort key not in sortMap, clear sort | ||
$options['order'] = []; | ||
$options['sort'] = null; | ||
unset($options['direction']); | ||
|
||
return $options; | ||
} | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The logic for handling sortMap vs traditional sorting creates nested conditional blocks that reduce readability. Consider extracting the sortMap handling into a separate method to improve code organization and maintainability.
Copilot uses AI. Check for mistakes.
Hmm, i'm probably not understanding it completely but 'sortMap' => [
'item1' => ['title', 'created']
'item2' => ['title', 'modified']
]
// sorting on both definitions:
&item1=asc&item2=desc Translates into the following statement right ? SORT BY
title ASC
created ASC,
title DESC,
modified DESC |
The paginatorhelper doesn't generate query strings like this. The direction can be specific only for 1 field, |
Imo we dont even need even sort asc/desc if we bind that into the key.
A flat list with all possible options |
Yes that could be an additional enhancement, though what I would really like is the ability to do multi field/colum sort. The query string for it could be like |
@ADmad I think that kind of sort style is not very readable for the end user. This is something Drupal (facets/views) does and it is constantly a fight to get right for proper SEO Maybe something like With that said, we should of course be weary not to go back to Cake 1-2.x style query params. Good times... 🫠 Another thought that crosses my mind is then making sure that it is always returned in the same order to keep a canonical structure. |
The average non-techie user doesn't read/understand query string params :)
I am not knowledgeable enough about how search engines deal with query string these days to be able to comment on this.
This could work I guess. BTW if for e.g. you have a search form with a multi-value field and using PRG you will get |
Yes. You ask a bad question, you get a bad answer. The SQL you posted is syntactically correct, and is what I would expect to happen if all of those ordering clauses were combined.
This could work but we'll need to make
No thank you 😄 |
* - `sortMap` - A map of sort keys to their corresponding database fields. Allows | ||
* creating friendly sort keys that map to one or more actual fields. When defined, | ||
* only the mapped keys will be sortable. Supports simple mapping, multi-column | ||
* sorting, and fixed direction sorting. You can also use numeric arrays for 1:1 | ||
* mappings where the field name is the same as the sort key. Example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has some overlap with sortableFields
should we keep both long term?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sortableFields
does become redundant with the addition of sortMap
, so it could be deprecated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think we can make this into something that is just as simple to use and handles more advanced use cases as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isnt it already now? :)
I added a demo of combined sort & dir. |
For me it looks quite complete. What's Missing: A way to specify default directions that can still be toggled. For example:
Potential Solution: We could enhance the syntax to support default directions that flip together: 'sortMap' => [
// New syntax: use '@' prefix for toggleable fields with defaults
'newest' => [
'@title' => 'asc', // Default asc, flips to desc
'@created' => 'desc', // Default desc, flips to asc
],
] When user clicks the sort link:
We could also invert this and make the @ signal that this is hardcoding the direction, as this might be more rare used.
Use ! to signal that this is hardcoded in that direction. |
I like the idea! |
Could we have classes to define how a field should be ordered instead of a new syntax? class OrderField
{
public function defaultDir(): ?string
{
}
public function isFixedDir(): bool
{
}
public function ormColumn(): array
} |
As opt-in? I feel like as default this could become quite some overhead for apps to create. |
or do you mean sth like namespace Cake\Datasource\Paging;
class SortField
{
protected string $field;
protected ?string $defaultDirection;
protected bool $locked;
public function __construct(string $field, ?string $defaultDirection = null, bool $locked = false)
{
$this->field = $field;
$this->defaultDirection = $defaultDirection;
$this->locked = $locked;
}
public static function asc(string $field): self
{
return new self($field, 'asc', false);
}
public static function desc(string $field): self
{
return new self($field, 'desc', false);
}
public static function locked(string $field, string $direction): self
{
return new self($field, $direction, true);
}
public function getField(): string
{
return $this->field;
}
public function getDirection(string $requestedDirection, bool $directionSpecified): string
{
if ($this->locked) {
return $this->defaultDirection;
}
if (!$directionSpecified && $this->defaultDirection) {
return $this->defaultDirection;
}
return $requestedDirection;
}
public function isLocked(): bool
{
return $this->locked;
}
} Usage Examples: 'sortMap' => [
'newest' => [
SortField::desc('created'), // Default desc, toggleable
SortField::asc('title'), // Default asc, toggleable
],
'popular' => [
SortField::locked('score', 'desc'), // Always desc
'author', // Still support strings for BC
],
] |
Yes, I like that. Maybe add an interface for it with methods I'm not sure if the methods |
We could maybe also create factory class for building the sortmap then. This would allow for a more strict and readable way for defining them. $factory = SortMapFactory::create()
->field('title')
->field('created')
->field('rank', locked: true, default: 'desc')
->combo('newest', [
SortField::desc('created'),
SortField::asc('title'),
])
->combo('popular', [
SortField::desc('score'),
SortField::asc('comments_count'),
])
->combo('recently-updated', [
SortField::desc('modified'),
SortField::asc('title'),
])
->raw('alpha-group', [
'group_name' => 'asc',
'name' => 'asc',
]); |
I added factory and SortField now with tests. |
if ($value instanceof SortField) { | ||
$field = $value->getField(); | ||
$fieldDirection = $value->getDirection($direction, $directionSpecified); | ||
$order[$field] = $fieldDirection; | ||
} elseif (is_int($key)) { | ||
// Indexed array: field uses querystring direction | ||
// e.g., ['modified', 'name'] | ||
$order[$value] = $direction; | ||
} elseif (str_ends_with($value, '!')) { | ||
// Associative array: check for locked (!) or default direction | ||
// Locked direction (ends with !): always use specified direction | ||
// e.g., ['created' => 'desc!'] always sorts desc | ||
$order[$key] = rtrim($value, '!'); | ||
} elseif (!$directionSpecified) { | ||
// Default direction that can be toggled | ||
// No direction specified, use the default | ||
$order[$key] = $value; | ||
} else { | ||
// Direction specified, use it for all toggleable fields | ||
$order[$key] = $direction; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we have both the data-object pattern, and array shaped data format. Can we pick one API? This is net new code, we shouldn't start off with two different APIs.
To me the data-object pattern is easier to operate as I can use LSP to explore it more easily. It is also less ambiguous and easier to evolve and extend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am fine with only factories and objects.
We can then remove all array configs.
/** | ||
* Represents a sort field configuration for pagination. | ||
*/ | ||
class SortField |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think classes like this are a great way for use to provide stronger typing and easier to operate and remember APIs.
// Check if we should use combined format | ||
$sortFormat = $this->getConfig('options.sortFormat', 'separate'); | ||
if ($sortFormat === 'combined') { | ||
// Use combined format: field-asc or field-desc | ||
$paging = ['sort' => $key . '-' . $dir, 'page' => 1]; | ||
} else { | ||
// Use traditional separate format | ||
$paging = ['sort' => $key, 'direction' => $dir, 'page' => 1]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this if we're introducing SortField
?
$sortMap = SortMapFactory::create() | ||
->sortKey('newest') | ||
->desc('created') | ||
->asc('title') | ||
->sortKey('oldest') | ||
->asc('created') | ||
->asc('title') | ||
->sortKey('popular') | ||
->locked('score', SortField::DESC) | ||
->desc('views') | ||
->sortKey('alphabetical') | ||
->asc('name') | ||
->build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need two different builder patterns? Can we pick one instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which ones u prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
i like the SortMapFactory,, i'm just not sure about the fluent style of building. This is something that is not used in other cake api's i think and seems a bit too locked in.
-
I'd additionally love to see something that would expose an instance of the factory through a callable, this way you would not need to call ->build()
'sortMap' => function(Builder $builder) {
return $builder
->key('newest', SortField::desc('title'), SortField::asc('title'))
->key('oldest', SortField::asc('created'), SortField::asc('title'))
->key('popular',
SortField::desc('score', locked: true),
SortField::desc('view'),
....
....
);
});
-
i would not use the word 'sort' (sortKey) in method names, This seems a bit redundant given the Factory shares the same name :-)
-
Maybe we should just use use
'sorts
as key to define the sortMap ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which ones u prefer?
I find the factory easier to understand when reading code. The fluent builder is interesting, but the method chains are dense, and I could see a stray sortKey() call causing unexpected results.
Resolves #18897
Feel free to further adjust.