Test Failed
Branch feature/v1_stable_fixes (e805e7)
by Diego
04:22
created

SearchableTrait::addRelations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Blackmine\Repository;
6
7
use Blackmine\Client\ClientInterface;
8
use Blackmine\Exception\Api\AbstractApiException;
9
use Carbon\CarbonInterface;
10
use Doctrine\Common\Collections\ArrayCollection;
11
use Doctrine\Common\Collections\Collection;
12
use JsonException;
13
use Blackmine\Model\CustomField;
14
15
trait SearchableTrait
16
{
17
    protected static array $filter_params = [];
18
    protected static array $sort_params = [];
19
    protected static array $search_params = [];
20
21
    protected int $limit = RepositoryInterface::DEFAULT_LIMIT;
22
    protected int $offset = RepositoryInterface::DEFAULT_OFFSET;
23
24
    /**
25
     * Adds a supported filter to the query.
26
     *
27
     * @param string $filter_name
28
     * @param mixed $value
29
     * @return SearchableTrait|AbstractSearchableRepository|CacheableRepository
30
     */
31
    public function addFilter(string $filter_name, mixed $value): self
32
    {
33
        if ($this->isAllowed($filter_name) && $this->checkType($value, $filter_name)) {
34
            static::$filter_params[$filter_name] = $value;
35
        }
36
37
        return $this;
38
    }
39
40
    /**
41
     * Adds a custom field to filter for to the search query.
42
     *
43
     * @param CustomField $cf
44
     * @return SearchableTrait|AbstractRepository|CacheableRepository
45
     */
46
    public function addCustomFieldFilter(CustomField $cf): self
47
    {
48
        if ($this->isAllowed(RepositoryInterface::COMMON_FILTER_CUSTOM_FIELDS)) {
49
            static::$filter_params[RepositoryInterface::COMMON_FILTER_CUSTOM_FIELDS][] = $cf;
50
        }
51
52
        return $this;
53
    }
54
55
    /**
56
     * Adds a starting date range filter to the query.
57
     *
58
     * @param CarbonInterface $date
59
     * @param string $date_field
60
     * @return SearchableTrait|AbstractRepository|CacheableRepository
61
     */
62
    public function from(CarbonInterface $date, string $date_field = self::COMMON_FILTER_UPDATED_ON): self
0 ignored issues
show
Bug introduced by
The constant Blackmine\Repository\Sea...OMMON_FILTER_UPDATED_ON was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
63
    {
64
        static::$filter_params[$date_field][RepositoryInterface::SEARCH_PARAM_FROM] = $date;
65
        return $this;
66
    }
67
68
    /**
69
     * Adds an ending date range filter to the query.
70
     *
71
     * @param CarbonInterface $date
72
     * @param string $date_field
73
     * @return SearchableTrait|AbstractRepository|CacheableRepository
74
     */
75
    public function to(CarbonInterface $date, string $date_field = self::COMMON_FILTER_UPDATED_ON): self
0 ignored issues
show
Bug introduced by
The constant Blackmine\Repository\Sea...OMMON_FILTER_UPDATED_ON was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
76
    {
77
        static::$filter_params[$date_field][RepositoryInterface::SEARCH_PARAM_TO] = $date;
78
        return $this;
79
    }
80
81
    /**
82
     * Adds sorting field and sorting direction to the query.
83
     *
84
     * @param string $field_name
85
     * @param string $direction
86
     * @return SearchableTrait|AbstractRepository|CacheableRepository
87
     */
88
    public function sortBy(string $field_name, string $direction = RepositoryInterface::SORT_DIRECTION_ASC): self
89
    {
90
        static::$sort_params[$field_name] = $direction;
91
        return $this;
92
    }
93
94
    /**
95
     * Adds limit to the query.
96
     *
97
     * @param int $limit
98
     * @return SearchableTrait|AbstractRepository|CacheableRepository
99
     */
100
    public function limit(int $limit): self
101
    {
102
        $this->limit = $limit;
103
        return $this;
104
    }
105
106
    /**
107
     * Adds starting results offset to the query.
108
     *
109
     * @param int $offset
110
     * @return SearchableTrait|AbstractRepository|CacheableRepository
111
     */
112
    public function offset(int $offset): self
113
    {
114
        $this->offset = $offset;
115
        return $this;
116
    }
117
118
    /**
119
     * Executes the search query.
120
     *
121
     * @return Collection
122
     * @throws AbstractApiException
123
     * @throws JsonException
124
     */
125
    public function search(): Collection
126
    {
127
        return $this->doSearch();
128
    }
129
130
    protected function reset(): self
131
    {
132
        static::$filter_params = [];
133
        return $this;
134
    }
135
136
    /**
137
     * @throws JsonException
138
     * @throws AbstractApiException
139
     */
140
    protected function doSearch(): Collection
141
    {
142
        $ret = new ArrayCollection();
143
144
        $search_endpoint = $this->getEndpoint() . "." . $this->getClient()->getFormat();
145
146
        $this->sanitizeParams();
147
        static::$search_params = $this->normalizeParams(static::$filter_params);
148
        static::$search_params = $this->addOrdering(static::$search_params);
149
        static::$search_params = $this->addRelations(static::$search_params);
0 ignored issues
show
Bug introduced by
The method addRelations() does not exist on Blackmine\Repository\SearchableTrait. Did you maybe mean addRelationToFetch()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
        /** @scrutinizer ignore-call */ 
150
        static::$search_params = $this->addRelations(static::$search_params);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
150
151
        while ($this->limit > 0) {
152
            if ($this->limit > 100) {
153
                $_limit = 100;
154
                $this->limit -= 100;
155
            } else {
156
                $_limit = $this->limit;
157
                $this->limit = 0;
158
            }
159
160
            static::$search_params[RepositoryInterface::SEARCH_PARAM_LIMIT] = $_limit;
161
            static::$search_params[RepositoryInterface::SEARCH_PARAM_OFFSET] = $this->offset;
162
163
            $api_response = $this->getClient()->get(
164
                $this->constructEndpointUrl($search_endpoint, static::$search_params)
165
            );
166
167
            if ($api_response->isSuccess()) {
168
                $ret = $this->getCollection($api_response->getData()[$this->getEndpoint()]);
0 ignored issues
show
Bug introduced by
It seems like getCollection() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
                /** @scrutinizer ignore-call */ 
169
                $ret = $this->getCollection($api_response->getData()[$this->getEndpoint()]);
Loading history...
169
                $this->offset += $_limit;
170
            } else {
171
                throw AbstractApiException::fromApiResponse($api_response);
172
            }
173
        }
174
175
        return $ret;
176
    }
177
178
    protected function isValidParameter(mixed $parameter, string $parameter_name): bool
179
    {
180
        $is_valid = false !== $parameter && null !== $parameter && '' !== $parameter;
181
182
        if (!empty($this->getAllowedFilters())) {
183
            return $is_valid && array_key_exists($parameter_name, $this->getAllowedFilters());
184
        }
185
186
        return $is_valid;
187
    }
188
189
    protected function sanitizeParams(): array
190
    {
191
        return array_filter(
192
            static::$filter_params,
193
            [$this, 'isValidParameter'],
194
            ARRAY_FILTER_USE_BOTH
195
        );
196
    }
197
198
    protected function checkType(mixed $value, string $parameter_name): bool
199
    {
200
        $expected_type = $this->getAllowedFilters()[$parameter_name];
201
202
        if (is_object($value)) {
203
            return get_class($value) === $expected_type;
204
        }
205
206
        if (is_array($value)) {
207
            return $this->isArrayType($expected_type) && $this->isValidArray($value, substr($expected_type, 0, -2));
208
        }
209
210
        return gettype($value) === $expected_type;
211
    }
212
213
    protected function normalizeParams(array $raw_params): array
214
    {
215
        $params = [];
216
        foreach ($raw_params as $parameter_name => $raw_value) {
217
            switch ($this->getAllowedFilters()[$parameter_name]) {
218
                case RepositoryInterface::SEARCH_PARAM_TYPE_INT:
219
                case RepositoryInterface::SEARCH_PARAM_TYPE_STRING:
220
                case RepositoryInterface::SEARCH_PARAM_TYPE_BOOL:
221
                default:
222
                    $params[$parameter_name] = $raw_value;
223
                    break;
224
                case RepositoryInterface::SEARCH_PARAM_TYPE_INT_ARRAY:
225
                case RepositoryInterface::SEARCH_PARAM_TYPE_STRING_ARRAY:
226
                    $params[$parameter_name] = implode(",", $raw_value);
227
                    break;
228
                case RepositoryInterface::SEARCH_PARAM_TYPE_CF_ARRAY:
229
                    foreach ($raw_value as $cf) {
230
                        $params["cf_" . $cf->getId()] = $cf->getValue();
231
                    }
232
                    break;
233
                case CarbonInterface::class:
234
                    $params[$parameter_name] = $this->addDateFilters($raw_value);
235
            }
236
        }
237
238
        return $params;
239
    }
240
241
    protected function addDateFilters(array | CarbonInterface $value): ?string
242
    {
243
        if ($value instanceof CarbonInterface) {
0 ignored issues
show
introduced by
$value is never a sub-type of Carbon\CarbonInterface.
Loading history...
244
            return $value->format("Y-m-d");
245
        }
246
247
        if (isset($value[RepositoryInterface::SEARCH_PARAM_FROM], $value[RepositoryInterface::SEARCH_PARAM_TO])) {
248
            $from = $value[RepositoryInterface::SEARCH_PARAM_FROM]->format("Y-m-d");
249
            $to = $value[RepositoryInterface::SEARCH_PARAM_TO]->format("Y-m-d");
250
            return "><" .  $from . "|" . $to;
251
        }
252
253
        if (isset($value[RepositoryInterface::SEARCH_PARAM_FROM])) {
254
            $from = $value[RepositoryInterface::SEARCH_PARAM_FROM]->format("Y-m-d");
255
            return ">=" . $from;
256
        }
257
258
        if (isset($value[RepositoryInterface::SEARCH_PARAM_TO])) {
259
            $to = $value[RepositoryInterface::SEARCH_PARAM_TO]->format("Y-m-d");
260
            return "<=" . $to;
261
        }
262
263
        return null;
264
    }
265
266
    protected function addOrdering(array $params): array
267
    {
268
        if (!empty(static::$sort_params)) {
269
            $ordering = [];
270
            foreach (static::$sort_params as $field => $direction) {
271
                if ($direction === RepositoryInterface::SORT_DIRECTION_DESC) {
272
                    $ordering[] = $field . ":" . $direction;
273
                } else {
274
                    $ordering[] = $field;
275
                }
276
            }
277
278
            $params[RepositoryInterface::SEARCH_PARAM_SORT] = implode(",", $ordering);
279
        }
280
281
        return $params;
282
    }
283
284
285
    protected function isAllowed(string $filter_name): bool
286
    {
287
        return array_key_exists($filter_name, $this->getAllowedFilters());
288
    }
289
290
    protected function isArrayType(string $type): bool
291
    {
292
        return str_ends_with($type, "[]");
293
    }
294
295
    protected function isValidArray(array $data, string $expected_type): bool
296
    {
297
        $is_valid = true;
298
        foreach ($data as $value) {
299
            if (gettype($value) !== $expected_type) {
300
                return false;
301
            }
302
        }
303
304
        return $is_valid;
305
    }
306
307
    /**
308
     * @return ClientInterface
309
     * @ignore
310
     */
311
    abstract public function getClient(): ClientInterface;
312
313
    /**
314
     * @return string
315
     * @ignore
316
     */
317
    abstract public function getEndpoint(): string;
318
319
    /**
320
     * @param string $endpoint
321
     * @param array $params
322
     * @return string
323
     * @ignore
324
     */
325
    abstract public function constructEndpointUrl(string $endpoint, array $params): string;
326
327
    /**
328
     * @return string
329
     * @ignore
330
     */
331
    abstract public function getModelClass(): string;
332
333
    /**
334
     * @return array
335
     * @ignore
336
     */
337
    abstract public function getAllowedFilters(): array;
338
339
    /**
340
     * @param string $relation
341
     * @ignore
342
     */
343
    abstract public function addRelationToFetch(string $relation): void;
344
}
345