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