Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Positions
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orderChildren
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 orderEntries
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 move
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Node;
6
7use Celema\Quma\Database;
8use Cosray\Exception\RuntimeException;
9
10/**
11 * Writes manual node orders. A scope is either the children of one parent
12 * or the top level of one collection, and every scope keeps its own order,
13 * so a node can hold a position in several. Positions are structure, not
14 * content: they bypass content versions, history and working copies and
15 * take effect immediately.
16 *
17 * Callers pass the complete order of a scope, which is renumbered 1..n.
18 * Rows of members missing from it are dropped, so members without a
19 * position sort after the positioned ones.
20 *
21 * @api
22 */
23final class Positions
24{
25    public function __construct(
26        private readonly Database $db,
27    ) {}
28
29    /** @param list<string> $uids the parent's children in their new order */
30    public function orderChildren(string $parent, array $uids): void
31    {
32        $this->db->positions->orderChildren([
33            'parent' => $parent,
34            'uids' => json_encode(array_values($uids), JSON_THROW_ON_ERROR),
35        ])->run();
36    }
37
38    /** @param list<string> $uids the collection's top-level entries in their new order */
39    public function orderEntries(string $collection, array $uids): void
40    {
41        $this->db->positions->orderEntries([
42            'collection' => $collection,
43            'uids' => json_encode(array_values($uids), JSON_THROW_ON_ERROR),
44        ])->run();
45    }
46
47    /**
48     * The group with `$uid` moved directly before or after `$neighbour`.
49     * A neighbour rather than an index, because a listing may show only
50     * part of a group (a page, or the types a collection lists), where an
51     * index would be ambiguous.
52     *
53     * @param list<string> $group a scope's members in their current order
54     * @return list<string>
55     */
56    public static function move(array $group, string $uid, string $neighbour, bool $after): array
57    {
58        if ($uid === $neighbour) {
59            throw new RuntimeException('A node cannot be placed next to itself');
60        }
61
62        if (!in_array($uid, $group, true) || !in_array($neighbour, $group, true)) {
63            throw new RuntimeException('The node and its neighbour must belong to the same group');
64        }
65
66        $rest = array_values(array_filter($group, static fn(string $member): bool => $member !== $uid));
67        $index = (int) array_search($neighbour, $rest, true) + ($after ? 1 : 0);
68        array_splice($rest, $index, 0, [$uid]);
69
70        return $rest;
71    }
72}