diego-ninja /
blackmine
| 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
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
|
|||||||
| 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
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 |