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
introduced
by
![]() |
|||||||
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
|
|||||||
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
|
|||||||
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
![]() |
|||||||
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 |