99use Kirby \Cms \Files ;
1010use Kirby \Cms \Page ;
1111use Kirby \Cms \Pages ;
12+ use Kirby \Cms \User ;
13+ use Kirby \Cms \Users ;
1214use Kirby \Exception \DuplicateException ;
15+ use Kirby \Exception \Exception ;
1316use Kirby \Exception \InvalidArgumentException ;
1417use Kirby \Toolkit \A ;
1518
@@ -18,68 +21,69 @@ class Similar
1821 /**
1922 * Cache object
2023 *
21- * @var Cache $cache
24+ * @var Cache|null $cache
2225 */
23- protected static $ cache ;
26+ protected static ? Cache $ cache = null ;
2427
2528 /**
2629 * Delimiter
2730 *
2831 * @var string
2932 */
30- protected $ delimiter ;
33+ protected string $ delimiter ;
3134
3235 /**
3336 * Single field as string or multiple fields as array
3437 *
35- * @var mixed
38+ * @var string|array
3639 */
37- protected $ fields ;
40+ protected string | array $ fields ;
3841
3942 /**
4043 * Collection to search in
4144 *
42- * @var Files|Pages
45+ * @var Files|Pages|Users
4346 */
44- protected $ index ;
47+ protected Files | Pages | Users $ index ;
4548
4649 /**
4750 * Filter results by language
4851 *
4952 * @var bool
5053 */
51- protected $ languageFilter ;
54+ protected bool $ languageFilter ;
5255
5356 /**
5457 * Threshold for results to count
5558 *
5659 * @var float
5760 */
58- protected $ threshold ;
61+ protected float $ threshold ;
5962
6063 /**
6164 * Base object
6265 *
63- * @var File|Page $base File or page object.
66+ * @var File|Page|User $base File or page object.
6467 */
65- protected $ base ;
68+ protected File | Page | User $ base ;
6669
6770 /**
6871 * Collection type
6972 *
70- * @var Files|Pages $collection
73+ * @var Files|Pages|Users $collection
7174 */
72- protected $ collection ;
75+ protected Files | Pages | Users $ collection ;
7376
7477 /**
7578 * User options
7679 *
7780 * @var array
7881 */
79- protected $ options ;
82+ protected array $ options ;
8083
81- public function __construct ($ base , $ collection , array $ options )
84+ public function __construct (File | Page | User $ base , Files | Pages | Users $ collection , array $ options )
8285 {
86+
8387 $ defaults = option ('texnixe.similar.defaults ' );
8488 $ defaults ['index ' ] = $ base ->siblings (false );
8589 $ this ->options = array_merge ($ defaults , $ options );
@@ -88,7 +92,7 @@ public function __construct($base, $collection, array $options)
8892 $ this ->delimiter = $ this ->options ['delimiter ' ];
8993 $ this ->fields = $ this ->options ['fields ' ];
9094 $ this ->index = $ this ->options ['index ' ];
91- $ this ->languageFilter = $ this ->options ['delimiter ' ];
95+ $ this ->languageFilter = $ this ->options ['languageFilter ' ];
9296 $ this ->threshold = $ this ->options ['threshold ' ];
9397 }
9498
@@ -116,7 +120,7 @@ public static function flush(): bool
116120 {
117121 try {
118122 return static ::cache ()->flush ();
119- } catch (InvalidArgumentException $ e ) {
123+ } catch (InvalidArgumentException ) {
120124 return false ;
121125 }
122126 }
@@ -125,14 +129,15 @@ public static function flush(): bool
125129 /**
126130 * Returns the similarity index
127131 *
128- * @param mixed $item
132+ * @param File|Page $item
129133 * @param array $searchItems
130134 *
131135 * @return float
132136 */
133- protected function calculateSimilarityIndex ($ item , array $ searchItems ): float
137+ protected function calculateSimilarityIndex (File | Page | User $ item , array $ searchItems ): float
134138 {
135139 $ indices = [];
140+
136141 foreach ($ searchItems as $ field => $ value ) {
137142 $ itemFieldValues = $ item ->{$ field }()->split ($ this ->delimiter );
138143 $ intersection = count (array_intersect ($ value [$ field ], $ itemFieldValues ));
@@ -141,20 +146,22 @@ protected function calculateSimilarityIndex($item, array $searchItems): float
141146 $ indices [] = number_format ($ intersection / $ union * $ value ['factor ' ], 5 );
142147 }
143148 }
149+
144150 if (($ indexCount = count ($ indices )) !== 0 ) {
145151 return array_sum ($ indices ) / $ indexCount ;
146152 }
147153
148- return ( float ) 0 ;
154+ return 0. 0 ;
149155 }
150156
151157 /**
152158 * Fetches similar pages
153159 *
154- * @return Files|Pages
160+ * @return Files|Pages|Users
161+ * @throws Exception
155162 * @throws InvalidArgumentException
156163 */
157- public function getData ()
164+ public function getData (): Files | Pages | Users
158165 {
159166 // initialize new collection based on type
160167 $ similar = $ this ->collection ;
@@ -184,106 +191,95 @@ public function getData()
184191 *
185192 * @return array
186193 * @throws InvalidArgumentException
194+ * @throws Exception
187195 */
188196 protected function getSearchItems (): array
189197 {
190198 $ searchItems = [];
191199 $ fields = $ this ->fields ;
192- if (is_array ($ fields )) {
193- if (A::isAssociative ($ fields )) {
194- foreach ($ fields as $ field => $ factor ) {
195- if (is_string ($ field ) === false ) {
196- throw new InvalidArgumentException ('Field array must be simple array or associative array ' );
197- }
198- // only include fields that have values
199- $ values = $ this ->base ->{$ field }()->split ($ this ->delimiter );
200- if (count ($ values ) > 0 ) {
201- $ searchItems [$ field ][$ field ] = $ values ;
202- $ searchItems [$ field ]['factor ' ] = $ factor ;
203- }
204- }
205- } else {
206- foreach ($ fields as $ field ) {
207- // only include fields that have values
208- $ values = $ this ->base ->{$ field }()->split ($ this ->delimiter );
209- if (count ($ values ) > 0 ) {
210- $ searchItems [$ field ][$ field ] = $ values ;
211- $ searchItems [$ field ]['factor ' ] = 1 ;
212- }
213- }
214- }
200+
201+ if (!is_string ($ fields ) && !is_array ($ fields )) {
202+ throw new InvalidArgumentException ('Fields must be provided as string or array ' );
215203 }
204+
216205 if (is_string ($ fields )) {
217206 $ field = $ fields ;
218207 $ searchItems [$ field ][$ field ] = $ this ->base ->{$ field }()->split ($ this ->delimiter );
219208 $ searchItems [$ field ]['factor ' ] = 1 ;
209+
210+ return $ searchItems ;
220211 }
221- return $ searchItems ;
212+
213+ if (A::isAssociative ($ fields )) {
214+ return $ this ->searchItemsForAssociativeArray ($ fields );
215+ }
216+
217+ return $ this ->searchItemsForIndexArray ($ fields );
218+
222219 }
223220
224221 /**
225222 * Returns similar pages
226223 *
227- * @return Files|Pages
224+ * @return Files|Pages|Users
228225 * @throws DuplicateException
226+ * @throws Exception
229227 * @throws InvalidArgumentException
230228 * @throws JsonException
231229 */
232- public function getSimilar ()
230+ public function getSimilar (): Files | Pages | Users
233231 {
234232 // try to get data from the cache, else create new
235233 if (option ('texnixe.similar.cache ' ) === true && $ response = static ::cache ()->get (md5 ($ this ->version () . $ this ->base ->id () . json_encode ($ this ->options , JSON_THROW_ON_ERROR )))) {
236234 foreach ($ response as $ key => $ data ) {
237235 $ this ->collection ->add ($ key );
238236 }
239- $ similar = $ this ->collection ;
237+ return $ this ->collection ;
238+ }
239+
240240 // else fetch new data and store in cache
241- } else {
242- // make sure we store no old stuff in the cache
243- if (option ('texnixe.similar.cache ' ) === false ) {
244- static ::cache ()->flush ();
245- }
246- $ similar = $ this ->getData ();
247- static ::cache ()->set (
248- md5 ($ this ->version () . $ this ->base ->id () . json_encode ($ this ->options , JSON_THROW_ON_ERROR )),
249- $ similar ->toArray (),
250- option ('texnixe.similar.expires ' )
251- );
241+ // make sure we store no old stuff in the cache
242+ if (option ('texnixe.similar.cache ' ) === false ) {
243+ static ::cache ()->flush ();
252244 }
245+ $ this ->collection = $ this ->getData ();
246+ static ::cache ()->set (
247+ md5 ($ this ->version () . $ this ->base ->id () . json_encode ($ this ->options , JSON_THROW_ON_ERROR )),
248+ $ this ->collection ->toArray (),
249+ option ('texnixe.similar.expires ' )
250+ );
253251
254- return $ similar ;
255- }
252+ return $ this ->collection ;
256253
254+ }
257255
258256 /**
259257 * Filters items by Jaccard Index
260258 *
261259 * @param array $searchItems
262260 *
263- * @return Files|Pages|\Kirby\Toolkit\Collection
261+ * @return Files|Pages|Users
264262 */
265- protected function filterByJaccardIndex (array $ searchItems )
263+ protected function filterByJaccardIndex (array $ searchItems ): Files | Pages | Users
266264 {
267- return $ this ->index ->map (function ($ item ) use ($ searchItems ) {
268- $ item ->jaccardIndex = $ this ->calculateSimilarityIndex ($ item , $ searchItems );
269- return $ item ;
270- })->filterBy ('jaccardIndex ' , '>= ' , $ this ->threshold )->sortBy ('jaccardIndex ' , 'desc ' );
265+ return $ this ->index
266+ ->filter (fn ($ item ) => $ this ->calculateSimilarityIndex ($ item , $ searchItems ) >= $ this ->threshold )
267+ ->sortBy (fn ($ item ) => $ this ->calculateSimilarityIndex ($ item , $ searchItems ),'desc ' );
271268 }
272269
273270 /**
274271 * Filters collection by current language if $languageFilter set to true
275272 *
276273 * @param $similar
277274 *
278- * @return Files|Pages
275+ * @return Files|Pages|Users
279276 */
280- protected function filterByLanguage ($ similar )
277+ protected function filterByLanguage ($ similar ): Files | Pages | Users
281278 {
282279 if (kirby ()->multilang () === true && ($ language = kirby ()->language ())) {
283- $ similar = $ similar ->filter (function ($ item ) use ($ language ) {
284- return $ item ->translation ($ language ->code ())->exists ();
285- });
280+ $ similar = $ similar ->filter (fn ($ item ) => $ item ->translation ($ language ->code ())->exists ());
286281 }
282+
287283 return $ similar ;
288284 }
289285
@@ -296,4 +292,52 @@ public function version()
296292 {
297293 return Kirby::plugin ('texnixe/similar ' )->version ()[0 ];
298294 }
295+
296+ /**
297+ * Return seach items for associative array
298+ * @param array $fields
299+ * @return array
300+ * @throws InvalidArgumentException
301+ */
302+ private function searchItemsForAssociativeArray (array $ fields ): array
303+ {
304+ $ searchItems = [];
305+
306+ foreach ($ fields as $ field => $ factor ) {
307+ if (is_string ($ field ) === false ) {
308+ throw new InvalidArgumentException ('Field array must be simple array or associative array ' );
309+ }
310+ // only include fields that have values
311+ $ values = $ this ->base ->{$ field }()->split ($ this ->delimiter );
312+ if (count ($ values ) > 0 ) {
313+ $ searchItems [$ field ][$ field ] = $ values ;
314+ $ searchItems [$ field ]['factor ' ] = $ factor ;
315+ }
316+ }
317+
318+ return $ searchItems ;
319+ }
320+
321+ /**
322+ * Return search items for an indexed array
323+ *
324+ * @param array $fields
325+ * @return array
326+ */
327+ private function searchItemsForIndexArray (array $ fields ): array
328+ {
329+ $ searchItems = [];
330+
331+ foreach ($ fields as $ field ) {
332+ // only include fields that have values
333+ $ values = $ this ->base ->{$ field }()->split ($ this ->delimiter );
334+ if (count ($ values ) > 0 ) {
335+ $ searchItems [$ field ][$ field ] = $ values ;
336+ $ searchItems [$ field ]['factor ' ] = 1 ;
337+ }
338+ }
339+
340+ return $ searchItems ;
341+
342+ }
299343}
0 commit comments