Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

dereuromark
Copy link
Member

Resolves #18897

Feel free to further adjust.

@dereuromark dereuromark added this to the 5.3.0 milestone Sep 9, 2025
@josbeir
Copy link
Contributor

josbeir commented Sep 9, 2025

Looks good, should we handle what i mentioned here: #18897 (comment)

(sharing the same field name on multiple sortmaps)

@dereuromark
Copy link
Member Author

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 same fields in different combinations.

The concern about ambiguity is resolved by the design:

  • No ambiguity exists because direct field sorting is disabled when sortMap is defined
  • Each sort key (item1, item2) is self-contained and independent
  • The question "In what direction should title be sorted?" doesn't arise because title itself is not a valid sort key

@ADmad ADmad requested a review from Copilot September 9, 2025 13:13
Copy link

@Copilot Copilot AI left a 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.

Comment on lines 568 to 583
// 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 {
Copy link
Preview

Copilot AI Sep 9, 2025

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.

@josbeir
Copy link
Contributor

josbeir commented Sep 9, 2025

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 same fields in different combinations.

The concern about ambiguity is resolved by the design:

* No ambiguity exists because direct field sorting is disabled when sortMap is defined

* Each sort key (item1, item2) is self-contained and independent

* The question "In what direction should title be sorted?" doesn't arise because title itself is not a valid sort key

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 

@ADmad
Copy link
Member

ADmad commented Sep 9, 2025

&item1=asc&item2=desc

The paginatorhelper doesn't generate query strings like this. The direction can be specific only for 1 field, sort=item1&dir=asc

@dereuromark
Copy link
Member Author

dereuromark commented Sep 9, 2025

Imo we dont even need even sort asc/desc if we bind that into the key.

sort=title-asc
sort=title-desc

A flat list with all possible options

@ADmad
Copy link
Member

ADmad commented Sep 9, 2025

Imo we dont even need even sort asc/desc if we bind that into the key.

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 sort[f1]=asc&sort[f2]=desc

@josbeir
Copy link
Contributor

josbeir commented Sep 9, 2025

@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 /products?sort=price:asc,rating:desc would be more friendly ?
or /products?sort=+price,-rating

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.

@ADmad
Copy link
Member

ADmad commented Sep 9, 2025

I think that kind of sort style is not very readable for the end user

The average non-techie user doesn't read/understand query string params :)

This is something Drupal (facets/views) does and it is constantly a fight to get right for proper SEO

I am not knowledgeable enough about how search engines deal with query string these days to be able to comment on this.

Maybe something like /products?sort=price:asc,rating:desc would be more friendly

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 foo[] in the query string :)

@markstory
Copy link
Member

Translates into the following statement right ?

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.

A flat list with all possible options

This could work but we'll need to make PaginatorHelper aware of how to change direction. How would application code express that field-asc and field-desc are related and opposite directions?

With that said, we should of course be weary not to go back to Cake 1-2.x style query params. Good times... 🫠

No thank you 😄

Comment on lines +52 to +56
* - `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:
Copy link
Member

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?

Copy link
Member

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.

@dereuromark
Copy link
Member Author

I added a demo of combined sort & dir.
Wonder if that would work out better.

@dereuromark
Copy link
Member Author

dereuromark commented Sep 11, 2025

For me it looks quite complete.

What's Missing:

A way to specify default directions that can still be toggled. For example:

  • Default: title ASC, created DESC
  • When toggled: title DESC, created ASC

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:

  • First click: title ASC, created DESC (defaults)
  • Second click: title DESC, created ASC (inverted)

We could also invert this and make the @ signal that this is hardcoding the direction, as this might be more rare used.
Or, thinking about it (and in line of other languages, e.g. CSS),

  'newest' => [
      'title' => 'asc!',     // Only asc
      'created' => 'desc',  // Default desc, flips to asc
  ],

Use ! to signal that this is hardcoded in that direction.

@josbeir
Copy link
Contributor

josbeir commented Sep 11, 2025

For me it looks quite complete.

What's Missing:

A way to specify default directions that can still be toggled. For example:

* Default: title ASC, created DESC

* When toggled: title DESC, created ASC

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:

* First click: title ASC, created DESC (defaults)

* Second click: title DESC, created ASC (inverted)

We could also invert this and make the @ signal that this is hardcoding the direction, as this might be more rare used. Or, thinking about it (and in line of other languages, e.g. CSS),

  'newest' => [
      'title' => 'asc!',     // Only asc
      'created' => 'desc',  // Default desc, flips to asc
  ],

Use ! to signal that this is hardcoded in that direction.

I like the idea!

@rochamarcelo
Copy link
Contributor

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
}

@dereuromark
Copy link
Member Author

As opt-in? I feel like as default this could become quite some overhead for apps to create.

@dereuromark
Copy link
Member Author

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
      ],
  ]

@rochamarcelo
Copy link
Contributor

rochamarcelo commented Sep 12, 2025

Yes, I like that. Maybe add an interface for it with methods getField and isLocked.

I'm not sure if the methods desc and asc are necessary, but the names could be clear, something like defaultDesc and defaultAsc.

@josbeir
Copy link
Contributor

josbeir commented Sep 12, 2025

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',
    ]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants