1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /** |
||
6 | * This file is part of Scout Extended. |
||
7 | * |
||
8 | * (c) Algolia Team <[email protected]> |
||
9 | * |
||
10 | * For the full copyright and license information, please view the LICENSE |
||
11 | * file that was distributed with this source code. |
||
12 | */ |
||
13 | |||
14 | namespace Algolia\ScoutExtended\Settings; |
||
15 | |||
16 | use Algolia\AlgoliaSearch\SearchIndex; |
||
17 | use Algolia\ScoutExtended\Exceptions\ModelNotFoundException; |
||
18 | use Algolia\ScoutExtended\Repositories\RemoteSettingsRepository; |
||
19 | use Algolia\ScoutExtended\Searchable\Aggregator; |
||
20 | use Illuminate\Database\Eloquent\ModelNotFoundException as BaseModelNotFoundException; |
||
21 | use Illuminate\Database\QueryException; |
||
22 | use Illuminate\Support\Str; |
||
23 | use function in_array; |
||
24 | use function is_string; |
||
25 | |||
26 | /** |
||
27 | * @internal |
||
28 | */ |
||
29 | final class LocalFactory |
||
30 | { |
||
31 | /** |
||
32 | * @var \Algolia\ScoutExtended\Repositories\RemoteSettingsRepository |
||
33 | */ |
||
34 | private $remoteRepository; |
||
35 | |||
36 | /** |
||
37 | * @var string[] |
||
38 | */ |
||
39 | private static $customRankingKeys = [ |
||
40 | '*ed_at', |
||
41 | 'count_*', |
||
42 | '*_count', |
||
43 | 'number_*', |
||
44 | '*_number', |
||
45 | ]; |
||
46 | |||
47 | /** |
||
48 | * @var string[] |
||
49 | */ |
||
50 | private static $unsearchableAttributesKeys = [ |
||
51 | 'id', |
||
52 | '*_id', |
||
53 | 'id_*', |
||
54 | '*ed_at', |
||
55 | '*_count', |
||
56 | 'count_*', |
||
57 | 'number_*', |
||
58 | '*_number', |
||
59 | '*image*', |
||
60 | '*url*', |
||
61 | '*link*', |
||
62 | '*password*', |
||
63 | '*token*', |
||
64 | '*hash*', |
||
65 | ]; |
||
66 | |||
67 | /** |
||
68 | * @var string[] |
||
69 | */ |
||
70 | private static $attributesForFacetingKeys = [ |
||
71 | '*category*', |
||
72 | '*list*', |
||
73 | '*country*', |
||
74 | '*city*', |
||
75 | '*type*', |
||
76 | ]; |
||
77 | |||
78 | /** |
||
79 | * @var string[] |
||
80 | */ |
||
81 | private static $unretrievableAttributes = [ |
||
82 | '*password*', |
||
83 | '*token*', |
||
84 | '*secret*', |
||
85 | '*hash*', |
||
86 | ]; |
||
87 | |||
88 | /** |
||
89 | * @var string[] |
||
90 | */ |
||
91 | private static $unsearchableAttributesValues = [ |
||
92 | 'http://*', |
||
93 | 'https://*', |
||
94 | ]; |
||
95 | |||
96 | /** |
||
97 | * @var string[] |
||
98 | */ |
||
99 | private static $disableTypoToleranceOnAttributesKeys = [ |
||
100 | 'slug', |
||
101 | '*_slug', |
||
102 | 'slug_*', |
||
103 | '*code*', |
||
104 | '*sku*', |
||
105 | '*reference*', |
||
106 | ]; |
||
107 | |||
108 | /** |
||
109 | * SettingsFactory constructor. |
||
110 | * |
||
111 | * @param \Algolia\ScoutExtended\Repositories\RemoteSettingsRepository $remoteRepository |
||
112 | * |
||
113 | * @return void |
||
114 | */ |
||
115 | 4 | public function __construct(RemoteSettingsRepository $remoteRepository) |
|
116 | { |
||
117 | 4 | $this->remoteRepository = $remoteRepository; |
|
118 | 4 | } |
|
119 | |||
120 | /** |
||
121 | * Creates settings for the given model. |
||
122 | * |
||
123 | * @param \Algolia\AlgoliaSearch\SearchIndex $index |
||
124 | * @param string $model |
||
125 | * |
||
126 | * @return \Algolia\ScoutExtended\Settings\Settings |
||
127 | */ |
||
128 | 4 | public function create(SearchIndex $index, string $model): Settings |
|
129 | { |
||
130 | 4 | $attributes = $this->getAttributes($model); |
|
131 | 3 | $searchableAttributes = []; |
|
132 | 3 | $attributesForFaceting = []; |
|
133 | 3 | $customRanking = []; |
|
134 | 3 | $disableTypoToleranceOnAttributes = []; |
|
135 | 3 | $unretrievableAttributes = []; |
|
136 | 3 | foreach ($attributes as $key => $value) { |
|
137 | 3 | $key = (string) $key; |
|
138 | |||
139 | 3 | if ($this->isSearchableAttributes($key, $value)) { |
|
140 | 3 | $searchableAttributes[] = $key; |
|
141 | } |
||
142 | |||
143 | 3 | if ($this->isAttributesForFaceting($key, $value)) { |
|
144 | 3 | $attributesForFaceting[] = $key; |
|
145 | } |
||
146 | |||
147 | 3 | if ($this->isCustomRanking($key, $value)) { |
|
148 | 3 | $customRanking[] = "desc({$key})"; |
|
149 | } |
||
150 | |||
151 | 3 | if ($this->isDisableTypoToleranceOnAttributes($key, $value)) { |
|
152 | $disableTypoToleranceOnAttributes[] = $key; |
||
153 | } |
||
154 | |||
155 | 3 | if ($this->isUnretrievableAttributes($key, $value)) { |
|
156 | 3 | $unretrievableAttributes[] = $key; |
|
157 | } |
||
158 | } |
||
159 | |||
160 | $detectedSettings = [ |
||
161 | 3 | 'searchableAttributes' => ! empty($searchableAttributes) ? $searchableAttributes : null, |
|
162 | 3 | 'attributesForFaceting' => ! empty($attributesForFaceting) ? $attributesForFaceting : null, |
|
163 | 3 | 'customRanking' => ! empty($customRanking) ? $customRanking : null, |
|
164 | 3 | 'disableTypoToleranceOnAttributes' => ! empty($disableTypoToleranceOnAttributes) ? |
|
165 | $disableTypoToleranceOnAttributes : null, |
||
166 | 3 | 'unretrievableAttributes' => ! empty($unretrievableAttributes) ? $unretrievableAttributes : null, |
|
167 | 3 | 'queryLanguages' => array_unique([config('app.locale'), config('app.fallback_locale')]), |
|
168 | ]; |
||
169 | |||
170 | 3 | $settings = array_merge($this->remoteRepository->find($index)->compiled(), $detectedSettings); |
|
171 | |||
172 | 3 | return new Settings($settings, $this->remoteRepository->defaults()); |
|
173 | } |
||
174 | |||
175 | /** |
||
176 | * Checks if the given key/value is a 'searchableAttributes'. |
||
177 | * |
||
178 | * @param string $key |
||
179 | * @param mixed $value |
||
180 | * |
||
181 | * @return bool |
||
182 | */ |
||
183 | 3 | public function isSearchableAttributes(string $key, $value): bool |
|
184 | { |
||
185 | 3 | return ! is_object($value) && ! is_array($value) && |
|
186 | 3 | ! Str::is(self::$unsearchableAttributesKeys, $key) && |
|
187 | 3 | ! Str::is(self::$unsearchableAttributesValues, $value); |
|
188 | } |
||
189 | |||
190 | /** |
||
191 | * Checks if the given key/value is a 'attributesForFaceting'. |
||
192 | * |
||
193 | * @param string $key |
||
194 | * @param mixed $value |
||
195 | * |
||
196 | * @return bool |
||
197 | */ |
||
198 | 3 | public function isAttributesForFaceting(string $key, $value): bool |
|
199 | { |
||
200 | 3 | return Str::is(self::$attributesForFacetingKeys, $key); |
|
201 | } |
||
202 | |||
203 | /** |
||
204 | * Checks if the given key/value is a 'customRanking'. |
||
205 | * |
||
206 | * @param string $key |
||
207 | * @param mixed $value |
||
208 | * |
||
209 | * @return bool |
||
210 | */ |
||
211 | 3 | public function isCustomRanking(string $key, $value): bool |
|
212 | { |
||
213 | 3 | return Str::is(self::$customRankingKeys, $key); |
|
214 | } |
||
215 | |||
216 | /** |
||
217 | * Checks if the given key/value is a 'disableTypoToleranceOnAttributes'. |
||
218 | * |
||
219 | * @param string $key |
||
220 | * @param mixed $value |
||
221 | * |
||
222 | * @return bool |
||
223 | */ |
||
224 | 3 | public function isDisableTypoToleranceOnAttributes(string $key, $value): bool |
|
225 | { |
||
226 | 3 | return is_string($key) && Str::is(self::$disableTypoToleranceOnAttributesKeys, $key); |
|
227 | } |
||
228 | |||
229 | /** |
||
230 | * Checks if the given key/value is a 'unretrievableAttributes'. |
||
231 | * |
||
232 | * @param string $key |
||
233 | * @param mixed $value |
||
234 | * |
||
235 | * @return bool |
||
236 | */ |
||
237 | 3 | public function isUnretrievableAttributes(string $key, $value): bool |
|
238 | { |
||
239 | 3 | return is_string($key) && Str::is(self::$unretrievableAttributes, $key); |
|
240 | } |
||
241 | |||
242 | /** |
||
243 | * Tries to get attributes from the searchable class. |
||
244 | * |
||
245 | * @param string $searchable |
||
246 | * |
||
247 | * @return array |
||
248 | */ |
||
249 | 4 | private function getAttributes(string $searchable): array |
|
250 | { |
||
251 | 4 | $attributes = []; |
|
252 | |||
253 | 4 | if (in_array(Aggregator::class, class_parents($searchable), true)) { |
|
254 | foreach (($instance = new $searchable)->getModels() as $model) { |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
255 | $attributes = array_merge($attributes, $this->getAttributes($model)); |
||
256 | } |
||
257 | } else { |
||
258 | 4 | $instance = null; |
|
259 | |||
260 | try { |
||
261 | 4 | $instance = $searchable::firstOrFail(); |
|
262 | 1 | } catch (QueryException | BaseModelNotFoundException $e) { |
|
263 | 1 | throw tap(new ModelNotFoundException())->setModel($searchable); |
|
264 | } |
||
265 | |||
266 | 3 | $attributes = method_exists($instance, 'toSearchableArray') ? $instance->toSearchableArray() : |
|
267 | 3 | $instance->toArray(); |
|
268 | } |
||
269 | |||
270 | 3 | return $attributes; |
|
271 | } |
||
272 | } |
||
273 |