Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.45% |
21 / 22 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
| Sort | |
95.45% |
21 / 22 |
|
85.71% |
6 / 7 |
12 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| order | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| chooseCollation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| expression | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
| collation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| indexName | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| valid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Cosray\Title; |
| 6 | |
| 7 | use Celema\Quma\Database; |
| 8 | use Cosray\Exception\RuntimeException; |
| 9 | use Cosray\Field\Field; |
| 10 | use Cosray\Locale; |
| 11 | |
| 12 | /** Shared title expressions and collation selection for queries and indexes. */ |
| 13 | final class Sort |
| 14 | { |
| 15 | /** @var array<string, string>|null */ |
| 16 | private ?array $collations = null; |
| 17 | |
| 18 | public function __construct( |
| 19 | private readonly Database $db, |
| 20 | ) {} |
| 21 | |
| 22 | public function order(Locale $locale, string $column = 'title'): string |
| 23 | { |
| 24 | $this->collations ??= array_column($this->db->titleSort->collations()->all(), 'identifier', 'name'); |
| 25 | $collation = self::chooseCollation($locale->id, $this->collations); |
| 26 | $expression = self::expression($locale, $column); |
| 27 | |
| 28 | return $collation === null ? $expression : "({$expression}) COLLATE {$collation}"; |
| 29 | } |
| 30 | |
| 31 | /** @param array<string, string> $available Catalog names mapped to quoted SQL identifiers. */ |
| 32 | public static function chooseCollation(string $localeId, array $available): ?string |
| 33 | { |
| 34 | return $available[self::collation($localeId)] ?? $available['und-x-icu'] ?? null; |
| 35 | } |
| 36 | |
| 37 | /** Match Resolver::stored(), including locale fallback and whitespace-only titles. */ |
| 38 | public static function expression(Locale|string $locale, string $column = 'title'): string |
| 39 | { |
| 40 | $ids = $locale instanceof Locale ? [$locale->id, ...$locale->fallbacks()] : [$locale]; |
| 41 | $ids[] = Field::NEUTRAL_LOCALE; |
| 42 | $values = []; |
| 43 | |
| 44 | foreach (array_unique($ids) as $id) { |
| 45 | if (!self::valid($id)) { |
| 46 | throw new RuntimeException("Invalid title sort locale '{$id}'"); |
| 47 | } |
| 48 | |
| 49 | $value = "{$column}->>'{$id}'"; |
| 50 | $values[] = |
| 51 | "CASE WHEN jsonb_typeof({$column}->'{$id}') = 'string' " |
| 52 | . "AND BTRIM({$value}, E' \\t\\n\\r\\013') <> '' THEN {$value} END"; |
| 53 | } |
| 54 | |
| 55 | return 'COALESCE(' . implode(', ', $values) . ')'; |
| 56 | } |
| 57 | |
| 58 | public static function collation(string $localeId): string |
| 59 | { |
| 60 | return str_replace('_', '-', $localeId) . '-x-icu'; |
| 61 | } |
| 62 | |
| 63 | public static function indexName(string $localeId): string |
| 64 | { |
| 65 | if (!self::valid($localeId)) { |
| 66 | throw new RuntimeException("Invalid title sort locale '{$localeId}'"); |
| 67 | } |
| 68 | |
| 69 | return 'ix_nodes_title_' . str_replace('-', '_', $localeId); |
| 70 | } |
| 71 | |
| 72 | public static function valid(string $localeId): bool |
| 73 | { |
| 74 | return preg_match('/^[A-Za-z][A-Za-z0-9_-]*$/D', $localeId) === 1; |
| 75 | } |
| 76 | } |