Issues (14)

src/Repository/SearchableTrait.php (4 issues)

Labels
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 Blackmine\Model\AbstractModel;
10
use Carbon\CarbonInterface;
11
use Blackmine\Collection\IdentityCollection;
12
use Blackmine\Model\Identity;
13
use Doctrine\Common\Collections\ArrayCollection;
14
use JsonException;
15
use Blackmine\Model\CustomField;
16
17
trait SearchableTrait
18
{
19
    protected static array $filter_params = [];
20
    protected static array $sort_params = [];
21
    protected static array $search_params = [];
22
23
    protected int $limit = RepositoryInterface::DEFAULT_LIMIT;
24
    protected int $offset = RepositoryInterface::DEFAULT_OFFSET;
25
26
    protected array $fetch_relations = [];
27
28
    public function addFilter(string $filter_name, mixed $value): self
29
    {
30
        if ($this->isAllowed($filter_name) && $this->checkType($value, $filter_name)) {
31
            static::$filter_params[$filter_name] = $value;
32
        }
33
34
        return $this;
35
    }
36
37
    public function addCustomFieldFilter(CustomField $cf): self
38
    {
39
        if ($this->isAllowed(RepositoryInterface::COMMON_FILTER_CUSTOM_FIELDS)) {
40
            static::$filter_params[RepositoryInterface::COMMON_FILTER_CUSTOM_FIELDS][] = $cf;
41
        }
42
43
        return $this;
44
    }
45
46
47
    public function with(string | array $include): self
48
    {
49
        if (!is_array($include)) {
0 ignored issues
show
The condition is_array($include) is always true.
Loading history...
50
            $include = [$include];
51
        }
52
53
        foreach ($include as $item) {
54
            $this->addRelationToFetch($item);
55
        }
56
57
        return $this;
58
    }
59
60
    public function reset(): self
61
    {
62
        static::$filter_params = [];
63
        return $this;
64
    }
65
66
    public function from(CarbonInterface $date, string $date_field = self::COMMON_FILTER_UPDATED_ON): self
0 ignored issues
show
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...
67
    {
68
        static::$filter_params[RepositoryInterface::SEARCH_PARAM_FROM][$date_field] = $date;
69
        return $this;
70
    }
71
72
    public function to(CarbonInterface $date, string $date_field = self::COMMON_FILTER_UPDATED_ON): self
0 ignored issues
show
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...
73
    {
74
        static::$filter_params[RepositoryInterface::SEARCH_PARAM_TO][$date_field] = $date;
75
        return $this;
76
    }
77
78
    public function sortBy(string $field_name, string $direction = RepositoryInterface::SORT_DIRECTION_ASC): self
79
    {
80
        static::$sort_params[$field_name] = $direction;
81
        return $this;
82
    }
83
84
    public function limit(int $limit): self
85
    {
86
        $this->limit = $limit;
87
        return $this;
88
    }
89
90
    public function offset(int $offset): self
91
    {
92
        $this->offset = $offset;
93
        return $this;
94
    }
95
96
    /**
97
     * @throws JsonException
98
     * @throws AbstractApiException
99
     */
100
    protected function doSearch(): ArrayCollection
101
    {
102
        $ret = new ArrayCollection();
103
104
        $search_endpoint = $this->getEndpoint() . "." . $this->getClient()->getFormat();
105
106
        $this->sanitizeParams();
107
        static::$search_params = $this->normalizeParams(static::$filter_params);
108
        static::$search_params = $this->addOrdering(static::$search_params);
109
        static::$search_params = $this->addRelations(static::$search_params);
110
111
        while ($this->limit > 0) {
112
            if ($this->limit > 100) {
113
                $_limit = 100;
114
                $this->limit -= 100;
115
            } else {
116
                $_limit = $this->limit;
117
                $this->limit = 0;
118
            }
119
120
            static::$search_params[RepositoryInterface::SEARCH_PARAM_LIMIT] = $_limit;
121
            static::$search_params[RepositoryInterface::SEARCH_PARAM_OFFSET] = $this->offset;
122
123
            $api_response = $this->getClient()->get(
124
                $this->constructEndpointUrl($search_endpoint, static::$search_params)
125
            );
126
127
            if ($api_response->isSuccess()) {
128
                $ret = $this->getCollection($api_response->getData()[$this->getEndpoint()]);
129
                $this->offset += $_limit;
130
            } else {
131
                throw AbstractApiException::fromApiResponse($api_response);
132
            }
133
        }
134
135
        return $ret;
136
    }
137
138
    protected function getCollection(array $items): ArrayCollection
139
    {
140
        $elements = [];
141
142
        foreach ($items as $item) {
143
            $object_class = $this->getModelClass();
144
            $object = new $object_class();
145
            $object->fromArray($item);
146
147
            $this->hydrateRelations($object);
0 ignored issues
show
It seems like hydrateRelations() 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

147
            $this->/** @scrutinizer ignore-call */ 
148
                   hydrateRelations($object);
Loading history...
148
149
            $elements[] = $object;
150
        }
151
152
        if (!empty($elements) && $elements[0] instanceof Identity) {
153
            return new IdentityCollection($elements);
154
        }
155
156
        return new ArrayCollection($elements);
157
    }
158
159
    protected function isValidParameter(mixed $parameter, string $parameter_name): bool
160
    {
161
        $is_valid = false !== $parameter && null !== $parameter && '' !== $parameter;
162
163
        if (!empty($this->getAllowedFilters())) {
164
            return $is_valid && array_key_exists($parameter_name, $this->getAllowedFilters());
165
        }
166
167
        return $is_valid;
168
    }
169
170
    protected function sanitizeParams(): array
171
    {
172
        return array_filter(
173
            static::$filter_params,
174
            [$this, 'isValidParameter'],
175
            ARRAY_FILTER_USE_BOTH
176
        );
177
    }
178
179
    protected function checkType(mixed $value, string $parameter_name): bool
180
    {
181
        $expected_type = $this->getAllowedFilters()[$parameter_name];
182
183
        if (is_object($value)) {
184
            return get_class($value) === $expected_type;
185
        }
186
187
        if (is_array($value)) {
188
            return $this->isArrayType($expected_type) && $this->isValidArray($value, substr($expected_type, 0, -2));
189
        }
190
191
        return gettype($value) === $expected_type;
192
    }
193
194
    protected function normalizeParams(array $raw_params): array
195
    {
196
        $params = [];
197
        foreach ($raw_params as $parameter_name => $raw_value) {
198
            switch ($this->getAllowedFilters()[$parameter_name]) {
199
                case RepositoryInterface::SEARCH_PARAM_TYPE_INT:
200
                case RepositoryInterface::SEARCH_PARAM_TYPE_STRING:
201
                case RepositoryInterface::SEARCH_PARAM_TYPE_BOOL:
202
                default:
203
                    $params[$parameter_name] = $raw_value;
204
                    break;
205
                case RepositoryInterface::SEARCH_PARAM_TYPE_INT_ARRAY:
206
                case RepositoryInterface::SEARCH_PARAM_TYPE_STRING_ARRAY:
207
                    $params[$parameter_name] = implode(",", $raw_value);
208
                    break;
209
                case RepositoryInterface::SEARCH_PARAM_TYPE_CF_ARRAY:
210
                    foreach ($raw_value as $cf) {
211
                        $params["cf_" . $cf->getId()] = $cf->getValue();
212
                    }
213
                    break;
214
                case CarbonInterface::class:
215
                    $params[$parameter_name] = $raw_value->format("Y-m-d");
216
            }
217
        }
218
219
        return $params;
220
    }
221
222
    protected function addOrdering(array $params): array
223
    {
224
        if (!empty(static::$sort_params)) {
225
            $ordering = [];
226
            foreach (static::$sort_params as $field => $direction) {
227
                if ($direction === RepositoryInterface::SORT_DIRECTION_DESC) {
228
                    $ordering[] = $field . ":" . $direction;
229
                } else {
230
                    $ordering[] = $field;
231
                }
232
            }
233
234
            $params[RepositoryInterface::SEARCH_PARAM_SORT] = implode(",", $ordering);
235
        }
236
237
        return $params;
238
    }
239
240
    protected function addRelations(array $params): array
241
    {
242
        if (!empty($this->fetch_relations)) {
243
            $params["include"] = implode(",", $this->fetch_relations);
244
        }
245
246
        return $params;
247
    }
248
249
250
    protected function isAllowed(string $filter_name): bool
251
    {
252
        return array_key_exists($filter_name, $this->getAllowedFilters());
253
    }
254
255
    protected function isArrayType(string $type): bool
256
    {
257
        return str_ends_with($type, "[]");
258
    }
259
260
    protected function isValidArray(array $data, string $expected_type): bool
261
    {
262
        $is_valid = true;
263
        foreach ($data as $value) {
264
            if (gettype($value) !== $expected_type) {
265
                return false;
266
            }
267
        }
268
269
        return $is_valid;
270
    }
271
272
    abstract public function getClient(): ClientInterface;
273
    abstract public function getEndpoint(): string;
274
    abstract public function constructEndpointUrl(string $endpoint, array $params): string;
275
    abstract public function getModelClass(): string;
276
    abstract public function getAllowedFilters(): array;
277
    abstract public function addRelationToFetch(string $relation): void;
278
}
279