Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.82% covered (success)
98.82%
84 / 85
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetEntitiesAroundPositionEndpoint
98.82% covered (success)
98.82%
84 / 85
50.00% covered (danger)
50.00%
1 / 2
13
0.00% covered (danger)
0.00%
0 / 1
 handle
98.70% covered (success)
98.70%
76 / 77
0.00% covered (danger)
0.00%
0 / 1
10
 getOlzEntityPositionResult
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace Olz\Api\Endpoints;
4
5use Doctrine\Common\Collections\Criteria;
6use Doctrine\Common\Collections\Order;
7use Olz\Api\OlzTypedEndpoint;
8use Olz\Entity\Common\OlzEntity;
9use Olz\Entity\Common\PositionableInterface;
10use PhpTypeScriptApi\HttpError;
11
12/**
13 * TODO: Support key-of<self::SUPPORTED_ENTITY_FIELDS> in php-typescript-api.
14 *
15 * @phpstan-type OlzEntityPositionResult array{
16 *   id: int, // TODO: int<1, max>
17 *   position: ?float,
18 *   title: non-empty-string,
19 * }
20 *
21 * @phpstan-import-type OlzSearchableEntityType from SearchEntitiesEndpoint
22 *
23 * @extends OlzTypedEndpoint<
24 *   array{
25 *     entityType: OlzSearchableEntityType,
26 *     entityField: non-empty-string,
27 *     id?: ?int<1, max>,
28 *     position?: ?float,
29 *     filter?: ?array<non-empty-string, string>,
30 *   },
31 *   array{
32 *     before?: ?OlzEntityPositionResult,
33 *     this?: ?OlzEntityPositionResult,
34 *     after?: ?OlzEntityPositionResult,
35 *   }
36 * >
37 */
38class GetEntitiesAroundPositionEndpoint extends OlzTypedEndpoint {
39    public const FLOAT_EPSILON = 1e-6; // PHP_FLOAT_EPSILON does not work for doctrine...
40
41    protected function handle(mixed $input): mixed {
42        $this->checkPermission('any');
43
44        $entity_type = $input['entityType'];
45        $entity_class = SearchEntitiesEndpoint::SUPPORTED_ENTITY_TYPES[$entity_type];
46        $entity_field = $input['entityField'];
47        try {
48            $position_field = $entity_class::getPositionFieldName($entity_field);
49        } catch (\Throwable $th) {
50            throw new HttpError(400, "Invalid position field {$entity_field} for entity {$entity_type}{$th->getMessage()}");
51        }
52
53        $repo = $this->entityManager()->getRepository($entity_class);
54
55        $filter_criteria = [];
56        foreach ($input['filter'] ?? [] as $key => $value) {
57            try {
58                $filter_criteria[] = $entity_class::getCriteriaForFilter($key, $value);
59            } catch (\Throwable $th) {
60                throw new HttpError(400, "Invalid filter {$key} => {$value} for entity {$entity_type}{$th->getMessage()}");
61            }
62        }
63
64        $on_off_criteria = is_subclass_of($entity_class, OlzEntity::class) ? [
65            Criteria::expr()->eq('on_off', 1),
66        ] : [];
67
68        $id = $input['id'] ?? null;
69        $id_field_name = $entity_class::getIdFieldNameForSearch();
70        $id_criteria = $id ? [Criteria::expr()->eq($id_field_name, $id)] : [];
71
72        $position = $input['position'] ?? null;
73        $position_criteria = $position !== null ? [
74            Criteria::expr()->gt($position_field, $position - self::FLOAT_EPSILON),
75            Criteria::expr()->lt($position_field, $position + self::FLOAT_EPSILON),
76        ] : [];
77
78        if (count($id_criteria) === 0 && count($position_criteria) === 0) {
79            return [
80                'before' => null,
81                'this' => null,
82                'after' => null,
83            ];
84        }
85
86        $this_entity_criteria = Criteria::create()
87            ->where(Criteria::expr()->andX(
88                ...$on_off_criteria,
89                ...$filter_criteria,
90                ...$id_criteria,
91                ...$position_criteria,
92            ))
93            ->setFirstResult(0)
94            ->setMaxResults(1)
95        ;
96        [$this_entity] = $repo->matching($this_entity_criteria);
97
98        $this_position = $this_entity?->getPositionForEntityField($entity_field);
99        if ($this_position === null) {
100            return [
101                'before' => null,
102                'this' => $this->getOlzEntityPositionResult($entity_field, $this_entity),
103                'after' => null,
104            ];
105        }
106
107        $before_criteria = Criteria::create()
108            ->where(Criteria::expr()->andX(
109                Criteria::expr()->isNotNull($position_field),
110                Criteria::expr()->lt($position_field, $this_position - self::FLOAT_EPSILON),
111                ...$on_off_criteria,
112                ...$filter_criteria,
113            ))
114            ->orderBy([$position_field => Order::Descending])
115            ->setFirstResult(0)
116            ->setMaxResults(1)
117        ;
118        [$before_entity] = $repo->matching($before_criteria);
119
120        $after_criteria = Criteria::create()
121            ->where(Criteria::expr()->andX(
122                Criteria::expr()->isNotNull($position_field),
123                Criteria::expr()->gt($position_field, $this_position + self::FLOAT_EPSILON),
124                ...$on_off_criteria,
125                ...$filter_criteria,
126            ))
127            ->orderBy([$position_field => Order::Ascending])
128            ->setFirstResult(0)
129            ->setMaxResults(1)
130        ;
131        [$after_entity] = $repo->matching($after_criteria);
132
133        return [
134            'before' => $this->getOlzEntityPositionResult($entity_field, $before_entity),
135            'this' => $this->getOlzEntityPositionResult($entity_field, $this_entity),
136            'after' => $this->getOlzEntityPositionResult($entity_field, $after_entity),
137        ];
138    }
139
140    /** @return ?OlzEntityPositionResult */
141    protected function getOlzEntityPositionResult(string $entity_field, ?PositionableInterface $entity): ?array {
142        if (!$entity) {
143            return null;
144        }
145        $position = $entity->getPositionForEntityField($entity_field);
146        return [
147            'id' => $entity->getIdForSearch(),
148            'position' => $position,
149            'title' => $entity->getTitleForSearch() ?: '-',
150        ];
151    }
152}