Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetEntitiesAroundPositionEndpoint
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 3
210
0.00% covered (danger)
0.00%
0 / 1
 configure
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
110
 getOlzEntityPositionResult
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
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    public function configure(): void {
42        parent::configure();
43        $this->phpStanUtils->registerTypeImport(SearchEntitiesEndpoint::class);
44    }
45
46    protected function handle(mixed $input): mixed {
47        $this->checkPermission('any');
48
49        $entity_type = $input['entityType'];
50        $entity_class = SearchEntitiesEndpoint::SUPPORTED_ENTITY_TYPES[$entity_type];
51        $entity_field = $input['entityField'];
52        try {
53            $position_field = $entity_class::getPositionFieldName($entity_field);
54        } catch (\Throwable $th) {
55            throw new HttpError(400, "Invalid position field {$entity_field} for entity {$entity_type}{$th->getMessage()}");
56        }
57
58        $repo = $this->entityManager()->getRepository($entity_class);
59
60        $filter_criteria = [];
61        foreach ($input['filter'] ?? [] as $key => $value) {
62            try {
63                $filter_criteria[] = $entity_class::getCriteriaForFilter($key, $value);
64            } catch (\Throwable $th) {
65                throw new HttpError(400, "Invalid filter {$key} => {$value} for entity {$entity_type}{$th->getMessage()}");
66            }
67        }
68
69        $on_off_criteria = is_subclass_of($entity_class, OlzEntity::class) ? [
70            Criteria::expr()->eq('on_off', 1),
71        ] : [];
72
73        $id = $input['id'] ?? null;
74        $id_field_name = $entity_class::getIdFieldNameForSearch();
75        $id_criteria = $id ? [Criteria::expr()->eq($id_field_name, $id)] : [];
76
77        $position = $input['position'] ?? null;
78        $position_criteria = $position !== null ? [
79            Criteria::expr()->gt($position_field, $position - self::FLOAT_EPSILON),
80            Criteria::expr()->lt($position_field, $position + self::FLOAT_EPSILON),
81        ] : [];
82
83        if (count($id_criteria) === 0 && count($position_criteria) === 0) {
84            return [
85                'before' => null,
86                'this' => null,
87                'after' => null,
88            ];
89        }
90
91        $this_entity_criteria = Criteria::create()
92            ->where(Criteria::expr()->andX(
93                ...$on_off_criteria,
94                ...$filter_criteria,
95                ...$id_criteria,
96                ...$position_criteria,
97            ))
98            ->setFirstResult(0)
99            ->setMaxResults(1)
100        ;
101        [$this_entity] = $repo->matching($this_entity_criteria);
102
103        $this_position = $this_entity?->getPositionForEntityField($entity_field);
104        if ($this_position === null) {
105            return [
106                'before' => null,
107                'this' => $this->getOlzEntityPositionResult($entity_field, $this_entity),
108                'after' => null,
109            ];
110        }
111
112        $before_criteria = Criteria::create()
113            ->where(Criteria::expr()->andX(
114                Criteria::expr()->isNotNull($position_field),
115                Criteria::expr()->lt($position_field, $this_position - self::FLOAT_EPSILON),
116                ...$on_off_criteria,
117                ...$filter_criteria,
118            ))
119            ->orderBy([$position_field => Order::Descending])
120            ->setFirstResult(0)
121            ->setMaxResults(1)
122        ;
123        [$before_entity] = $repo->matching($before_criteria);
124
125        $after_criteria = Criteria::create()
126            ->where(Criteria::expr()->andX(
127                Criteria::expr()->isNotNull($position_field),
128                Criteria::expr()->gt($position_field, $this_position + self::FLOAT_EPSILON),
129                ...$on_off_criteria,
130                ...$filter_criteria,
131            ))
132            ->orderBy([$position_field => Order::Ascending])
133            ->setFirstResult(0)
134            ->setMaxResults(1)
135        ;
136        [$after_entity] = $repo->matching($after_criteria);
137
138        return [
139            'before' => $this->getOlzEntityPositionResult($entity_field, $before_entity),
140            'this' => $this->getOlzEntityPositionResult($entity_field, $this_entity),
141            'after' => $this->getOlzEntityPositionResult($entity_field, $after_entity),
142        ];
143    }
144
145    /** @return ?OlzEntityPositionResult */
146    protected function getOlzEntityPositionResult(string $entity_field, ?PositionableInterface $entity): ?array {
147        if (!$entity) {
148            return null;
149        }
150        $position = $entity->getPositionForEntityField($entity_field);
151        return [
152            'id' => $entity->getIdForSearch(),
153            'position' => $position,
154            'title' => $entity->getTitleForSearch() ?: '-',
155        ];
156    }
157}