]> BookStack Code Mirror - bookstack/blob - app/Api/ListingResponseBuilder.php
Default chapter templates: Updated api docs and tests
[bookstack] / app / Api / ListingResponseBuilder.php
1 <?php
2
3 namespace BookStack\Api;
4
5 use Illuminate\Database\Eloquent\Builder;
6 use Illuminate\Database\Eloquent\Collection;
7 use Illuminate\Database\Eloquent\Model;
8 use Illuminate\Http\JsonResponse;
9 use Illuminate\Http\Request;
10
11 class ListingResponseBuilder
12 {
13     protected Builder $query;
14     protected Request $request;
15
16     /**
17      * @var string[]
18      */
19     protected array $fields;
20
21     /**
22      * @var array<callable>
23      */
24     protected array $resultModifiers = [];
25
26     /**
27      * @var array<string, string>
28      */
29     protected array $filterOperators = [
30         'eq'   => '=',
31         'ne'   => '!=',
32         'gt'   => '>',
33         'lt'   => '<',
34         'gte'  => '>=',
35         'lte'  => '<=',
36         'like' => 'like',
37     ];
38
39     /**
40      * ListingResponseBuilder constructor.
41      * The given fields will be forced visible within the model results.
42      */
43     public function __construct(Builder $query, Request $request, array $fields)
44     {
45         $this->query = $query;
46         $this->request = $request;
47         $this->fields = $fields;
48     }
49
50     /**
51      * Get the response from this builder.
52      */
53     public function toResponse(): JsonResponse
54     {
55         $filteredQuery = $this->filterQuery($this->query);
56
57         $total = $filteredQuery->count();
58         $data = $this->fetchData($filteredQuery)->each(function ($model) {
59             foreach ($this->resultModifiers as $modifier) {
60                 $modifier($model);
61             }
62         });
63
64         dd($data->first());
65
66         return response()->json([
67             'data'  => $data,
68             'total' => $total,
69         ]);
70     }
71
72     /**
73      * Add a callback to modify each element of the results.
74      *
75      * @param (callable(Model): void) $modifier
76      */
77     public function modifyResults(callable $modifier): void
78     {
79         $this->resultModifiers[] = $modifier;
80     }
81
82     /**
83      * Fetch the data to return within the response.
84      */
85     protected function fetchData(Builder $query): Collection
86     {
87         $query = $this->countAndOffsetQuery($query);
88         $query = $this->sortQuery($query);
89
90         return $query->get($this->fields);
91     }
92
93     /**
94      * Apply any filtering operations found in the request.
95      */
96     protected function filterQuery(Builder $query): Builder
97     {
98         $query = clone $query;
99         $requestFilters = $this->request->get('filter', []);
100         if (!is_array($requestFilters)) {
101             return $query;
102         }
103
104         $queryFilters = collect($requestFilters)->map(function ($value, $key) {
105             return $this->requestFilterToQueryFilter($key, $value);
106         })->filter(function ($value) {
107             return !is_null($value);
108         })->values()->toArray();
109
110         return $query->where($queryFilters);
111     }
112
113     /**
114      * Convert a request filter query key/value pair into a [field, op, value] where condition.
115      */
116     protected function requestFilterToQueryFilter($fieldKey, $value): ?array
117     {
118         $splitKey = explode(':', $fieldKey);
119         $field = $splitKey[0];
120         $filterOperator = $splitKey[1] ?? 'eq';
121
122         if (!in_array($field, $this->fields)) {
123             return null;
124         }
125
126         if (!in_array($filterOperator, array_keys($this->filterOperators))) {
127             $filterOperator = 'eq';
128         }
129
130         $queryOperator = $this->filterOperators[$filterOperator];
131
132         return [$field, $queryOperator, $value];
133     }
134
135     /**
136      * Apply sorting operations to the query from given parameters
137      * otherwise falling back to the first given field, ascending.
138      */
139     protected function sortQuery(Builder $query): Builder
140     {
141         $query = clone $query;
142         $defaultSortName = $this->fields[0];
143         $direction = 'asc';
144
145         $sort = $this->request->get('sort', '');
146         if (strpos($sort, '-') === 0) {
147             $direction = 'desc';
148         }
149
150         $sortName = ltrim($sort, '+- ');
151         if (!in_array($sortName, $this->fields)) {
152             $sortName = $defaultSortName;
153         }
154
155         return $query->orderBy($sortName, $direction);
156     }
157
158     /**
159      * Apply count and offset for paging, based on params from the request while falling
160      * back to system defined default, taking the max limit into account.
161      */
162     protected function countAndOffsetQuery(Builder $query): Builder
163     {
164         $query = clone $query;
165         $offset = max(0, $this->request->get('offset', 0));
166         $maxCount = config('api.max_item_count');
167         $count = $this->request->get('count', config('api.default_item_count'));
168         $count = max(min($maxCount, $count), 1);
169
170         return $query->skip($offset)->take($count);
171     }
172 }