Completed
Pull Request — master (#259)
by
unknown
01:18
created

TNTSearchEngine   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 452
Duplicated Lines 2.43 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 10
dl 11
loc 452
rs 6
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A delete() 6 17 2
A search() 0 8 2
A paginate() 0 26 4
B performSearch() 0 37 10
B map() 0 30 6
A getBuilder() 0 13 2
A mapIds() 0 4 1
A getTotalCount() 0 4 1
A initIndex() 0 16 3
A discardIdsFromResultSetByConstraints() 0 20 1
A usesSoftDelete() 0 4 1
A handleSoftDeletes() 0 32 5
A applyWheres() 0 12 1
A applyOrders() 0 12 1
A flush() 0 15 4
B update() 5 44 10

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like TNTSearchEngine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TNTSearchEngine, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TeamTNT\Scout\Engines;
4
5
use Illuminate\Database\Eloquent\Collection;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Facades\DB;
8
use Laravel\Scout\Builder;
9
use Laravel\Scout\Engines\Engine;
10
use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException;
11
use TeamTNT\TNTSearch\TNTGeoSearch;
12
use TeamTNT\TNTSearch\TNTSearch;
13
14
class TNTSearchEngine extends Engine
15
{
16
    /**
17
     * @var TNTSearch
18
     */
19
    protected $tnt;
20
21
    /**
22
     * @var TNTGeoSearch
23
     */
24
    protected $geotnt;
25
26
    /**
27
     * @var Builder
28
     */
29
    protected $builder;
30
31
    /**
32
     * Create a new engine instance.
33
     *
34
     * @param TNTSearch $tnt
35
     * @param TNTGeoSearch|null $geotnt
36
     */
37
    public function __construct(TNTSearch $tnt, TNTGeoSearch $geotnt = null)
38
    {
39
        $this->tnt = $tnt;
40
        $this->geotnt = $geotnt;
41
    }
42
43
    /**
44
     * Update the given model in the index.
45
     *
46
     * @param Collection $models
47
     *
48
     * @return void
49
     */
50
    public function update($models)
51
    {
52
        $this->initIndex($models->first());
53
        $this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
54
        $index = $this->tnt->getIndex();
55
        $index->setPrimaryKey($models->first()->getKeyName());
56
57
        $geoindex = null;
58 View Code Duplication
        if ($this->geotnt) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
59
            $this->geotnt->selectIndex("{$models->first()->searchableAs()}.index");
60
            $geoindex = $this->geotnt->getIndex();
61
            $geoindex->setPrimaryKey($models->first()->getKeyName());
62
        }
63
64
        $index->indexBeginTransaction();
65
        if ($geoindex) {
66
            $geoindex->indexBeginTransaction();
67
        }
68
        $models->each(function ($model) use ($index, $geoindex) {
69
            $array = $model->toSearchableArray();
70
71
            if (empty($array)) {
72
                return;
73
            }
74
75
            if ($model->getKey()) {
76
                if ($geoindex) {
77
                    $geoindex->delete($model->getKey());
78
                }
79
                $index->update($model->getKey(), $array);
80
            } else {
81
                $index->insert($array);
82
            }
83
            if ($geoindex && !empty($array['longitude']) && !empty($array['latitude'])) {
84
                $array['longitude'] = (float) $array['longitude'];
85
                $array['latitude'] = (float) $array['latitude'];
86
                $geoindex->insert($array);
87
            }
88
        });
89
        $index->indexEndTransaction();
90
        if ($geoindex) {
91
            $geoindex->indexEndTransaction();
92
        }
93
    }
94
95
    /**
96
     * Remove the given model from the index.
97
     *
98
     * @param Collection $models
99
     *
100
     * @return void
101
     */
102
    public function delete($models)
103
    {
104
        $this->initIndex($models->first());
105
        $models->each(function ($model) {
106
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
107
            $index = $this->tnt->getIndex();
108
            $index->setPrimaryKey($model->getKeyName());
109
            $index->delete($model->getKey());
110
111 View Code Duplication
            if ($this->geotnt) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112
                $this->geotnt->selectIndex("{$model->searchableAs()}.index");
113
                $index = $this->geotnt->getIndex();
114
                $index->setPrimaryKey($model->getKeyName());
115
                $index->delete($model->getKey());
116
            }
117
        });
118
    }
119
120
    /**
121
     * Perform the given search on the engine.
122
     *
123
     * @param Builder $builder
124
     *
125
     * @return mixed
126
     */
127
    public function search(Builder $builder)
128
    {
129
        try {
130
            return $this->performSearch($builder);
131
        } catch (IndexNotFoundException $e) {
132
            $this->initIndex($builder->model);
133
        }
134
    }
135
136
    /**
137
     * Perform the given search on the engine.
138
     *
139
     * @param Builder $builder
140
     * @param int     $perPage
141
     * @param int     $page
142
     *
143
     * @return mixed
144
     */
145
    public function paginate(Builder $builder, $perPage, $page)
146
    {
147
        $results = $this->performSearch($builder);
148
149
        if ($builder->limit) {
150
            $results['hits'] = $builder->limit;
151
        }
152
153
        $filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
154
155
        $results['hits'] = $filtered->count();
156
157
        $chunks = array_chunk($filtered->toArray(), $perPage);
158
159
        if (empty($chunks)) {
160
            return $results;
161
        }
162
163
        if (array_key_exists($page - 1, $chunks)) {
164
            $results['ids'] = $chunks[$page - 1];
165
        } else {
166
            $results['ids'] = [];
167
        }
168
169
        return $results;
170
    }
171
172
    /**
173
     * Perform the given search on the engine.
174
     *
175
     * @param Builder $builder
176
     *
177
     * @return mixed
178
     */
179
    protected function performSearch(Builder $builder, array $options = [])
180
    {
181
        $index = $builder->index ?: $builder->model->searchableAs();
182
        $limit = $builder->limit ?: 10000;
183
        $this->tnt->selectIndex("{$index}.index");
184
        if ($this->geotnt) {
185
            $this->geotnt->selectIndex("{$index}.geoindex");
186
        }
187
188
        $this->builder = $builder;
189
190
        if (isset($builder->model->asYouType)) {
191
            $this->tnt->asYouType = $builder->model->asYouType;
192
        }
193
194
        if ($builder->callback) {
195
            return call_user_func(
196
                $builder->callback,
197
                $this->tnt,
198
                $builder->query,
199
                $options
200
            );
201
        }
202
203
        if (is_array($builder->query)) {
204
            $location = $builder->query['location'];
205
            $distance = $builder->query['distance'];
206
            $limit = array_key_exists('limit', $builder->query) ? $builder->query['limit'] : 10;
207
            return $this->geotnt->findNearest($location, $distance, $limit);
208
        }
209
210
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
211
            return $this->tnt->searchBoolean($builder->query, $limit);
212
        } else {
213
            return $this->tnt->search($builder->query, $limit);
214
        }
215
    }
216
217
    /**
218
     * Map the given results to instances of the given model.
219
     *
220
     * @param mixed                               $results
221
     * @param \Illuminate\Database\Eloquent\Model $model
222
     *
223
     * @return Collection
224
     */
225
    public function map(Builder $builder, $results, $model)
226
    {
227
        if (is_null($results['ids']) || count($results['ids']) === 0) {
228
            return Collection::make();
229
        }
230
231
        $keys = collect($results['ids'])->values()->all();
232
233
        $builder = $this->getBuilder($model);
234
235
        if ($this->builder->queryCallback) {
236
            call_user_func($this->builder->queryCallback, $builder);
237
        }
238
239
        $models = $builder->whereIn(
240
            $model->getQualifiedKeyName(), $keys
241
        )->get()->keyBy($model->getKeyName());
242
243
        // sort models by user choice
244
        if (!empty($this->builder->orders)) {
245
            return $models->values();
246
        }
247
248
        // sort models by tnt search result set
249
        return collect($results['ids'])->map(function ($hit) use ($models) {
250
            if (isset($models[$hit])) {
251
                return $models[$hit];
252
            }
253
        })->filter()->values();
254
    }
255
256
    /**
257
     * Return query builder either from given constraints, or as
258
     * new query. Add where statements to builder when given.
259
     *
260
     * @param \Illuminate\Database\Eloquent\Model $model
261
     *
262
     * @return Builder
263
     */
264
    public function getBuilder($model)
265
    {
266
        // get query as given constraint or create a new query
267
        $builder = isset($this->builder->constraints) ? $this->builder->constraints : $model->newQuery();
0 ignored issues
show
Bug introduced by
The property constraints does not seem to exist in Laravel\Scout\Builder.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
268
269
        $builder = $this->handleSoftDeletes($builder, $model);
270
271
        $builder = $this->applyWheres($builder);
272
273
        $builder = $this->applyOrders($builder);
274
275
        return $builder;
276
    }
277
278
    /**
279
     * Pluck and return the primary keys of the given results.
280
     *
281
     * @param mixed $results
282
     * @return \Illuminate\Support\Collection
283
     */
284
    public function mapIds($results)
285
    {
286
        return collect($results['ids'])->values();
287
    }
288
289
    /**
290
     * Get the total count from a raw result returned by the engine.
291
     *
292
     * @param mixed $results
293
     *
294
     * @return int
295
     */
296
    public function getTotalCount($results)
297
    {
298
        return $results['hits'];
299
    }
300
301
    public function initIndex($model)
302
    {
303
        $indexName = $model->searchableAs();
304
305
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
306
            $indexer = $this->tnt->createIndex("$indexName.index");
307
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
308
            $indexer->setPrimaryKey($model->getKeyName());
309
310
            if (!$this->geotnt) {
311
                $indexer = $this->geotnt->getIndex()->createIndex("$indexName.geoindex");
312
                $indexer->setDatabaseHandle($model->getConnection()->getPdo());
313
                $indexer->setPrimaryKey($model->getKeyName());
314
            }
315
        }
316
    }
317
318
    /**
319
     * The search index results ($results['ids']) need to be compared against our query
320
     * that contains the constraints.
321
     *
322
     * To get the correct results and counts for the pagination, we remove those ids
323
     * from the search index results that were found by the search but are not part of
324
     * the query ($sub) that is constrained.
325
     *
326
     * This is achieved with self joining the constrained query as subquery and selecting
327
     * the ids which are not matching to anything (i.e., is null).
328
     *
329
     * The constraints usually remove only a small amount of results, which is why the non
330
     * matching results are looked up and removed, instead of returning a collection with
331
     * all the valid results.
332
     */
333
    private function discardIdsFromResultSetByConstraints($builder, $searchResults)
334
    {
335
        $qualifiedKeyName    = $builder->model->getQualifiedKeyName(); // tableName.id
336
        $subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
337
338
        $sub = $this->getBuilder($builder->model)->whereIn(
339
            $qualifiedKeyName, $searchResults
340
        ); // sub query for left join
341
342
        $discardIds = $builder->model->newQuery()
343
            ->select($qualifiedKeyName)
344
            ->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as '. $builder->model->getConnection()->getTablePrefix() .'sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
345
            ->addBinding($sub->getQuery()->getBindings(), 'join')
346
            ->whereIn($qualifiedKeyName, $searchResults)
347
            ->whereNull($subQualifiedKeyName)
348
            ->pluck($builder->model->getKeyName());
349
350
        // returns values of $results['ids'] that are not part of $discardIds
351
        return collect($searchResults)->diff($discardIds);
352
    }
353
354
    /**
355
     * Determine if the given model uses soft deletes.
356
     *
357
     * @param  \Illuminate\Database\Eloquent\Model  $model
358
     * @return bool
359
     */
360
    protected function usesSoftDelete($model)
361
    {
362
        return in_array(SoftDeletes::class, class_uses_recursive($model));
363
    }
364
365
    /**
366
     * Determine if soft delete is active and depending on state return the
367
     * appropriate builder.
368
     *
369
     * @param  Builder  $builder
370
     * @param  \Illuminate\Database\Eloquent\Model  $model
371
     * @return Builder
372
     */
373
    private function handleSoftDeletes($builder, $model)
374
    {
375
        // remove where statement for __soft_deleted when soft delete is not active
376
        // does not show soft deleted items when trait is attached to model and
377
        // config('scout.soft_delete') is false
378
        if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
379
            unset($this->builder->wheres['__soft_deleted']);
380
            return $builder;
381
        }
382
383
        /**
384
         * Use standard behaviour of Laravel Scout builder class to support soft deletes.
385
         *
386
         * When no __soft_deleted statement is given return all entries
387
         */
388
        if (!in_array('__soft_deleted', $this->builder->wheres)) {
389
            return $builder->withTrashed();
390
        }
391
392
        /**
393
         * When __soft_deleted is 1 then return only soft deleted entries
394
         */
395
        if ($this->builder->wheres['__soft_deleted']) {
396
            $builder = $builder->onlyTrashed();
397
        }
398
399
        /**
400
         * Returns all undeleted entries, default behaviour
401
         */
402
        unset($this->builder->wheres['__soft_deleted']);
403
        return $builder;
404
    }
405
406
    /**
407
     * Apply where statements as constraints to the query builder.
408
     *
409
     * @param Builder $builder
410
     * @return \Illuminate\Support\Collection
411
     */
412
    private function applyWheres($builder)
413
    {
414
        // iterate over given where clauses
415
        return collect($this->builder->wheres)->map(function ($value, $key) {
416
            // for reduce function combine key and value into array
417
            return [$key, $value];
418
        })->reduce(function ($builder, $where) {
419
            // separate key, value again
420
            list($key, $value) = $where;
421
            return $builder->where($key, $value);
422
        }, $builder);
423
    }
424
425
    /**
426
     * Apply order by statements as constraints to the query builder.
427
     *
428
     * @param Builder $builder
429
     * @return \Illuminate\Support\Collection
430
     */
431
    private function applyOrders($builder)
432
    {
433
        //iterate over given orderBy clauses - should be only one
434
        return collect($this->builder->orders)->map(function ($value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
435
            // for reduce function combine key and value into array
436
            return [$value["column"], $value["direction"]];
437
        })->reduce(function ($builder, $orderBy) {
438
            // separate key, value again
439
            list($column, $direction) = $orderBy;
440
            return $builder->orderBy($column, $direction);
441
        }, $builder);
442
    }
443
444
    /**
445
     * Flush all of the model's records from the engine.
446
     *
447
     * @param  \Illuminate\Database\Eloquent\Model  $model
448
     * @return void
449
     */
450
    public function flush($model)
451
    {
452
        $indexName   = $model->searchableAs();
453
        $pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
454
        if (file_exists($pathToIndex)) {
455
            unlink($pathToIndex);
456
        }
457
458
        if ($this->geotnt){
459
            $pathToGeoIndex = $this->geotnt->config['storage']."/{$indexName}.index";
460
            if (file_exists($pathToGeoIndex)) {
461
                unlink($pathToGeoIndex);
462
            }
463
        }
464
    }
465
}
466