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

TNTSearchEngine::delete()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 17

Duplication

Lines 6
Ratio 35.29 %

Importance

Changes 0
Metric Value
dl 6
loc 17
rs 9.7
c 0
b 0
f 0
cc 2
nc 1
nop 1
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()}.geoindex");
60
            $geoindex = $this->geotnt->getIndex();
61
            $geoindex->setPrimaryKey($models->first()->getKeyName());
62
            $geoindex->indexBeginTransaction();
63
        }
64
65
        $index->indexBeginTransaction();
66
        $models->each(function ($model) use ($index, $geoindex) {
67
            $array = $model->toSearchableArray();
68
69
            if (empty($array)) {
70
                return;
71
            }
72
73
            if ($model->getKey()) {
74
                $index->update($model->getKey(), $array);
75
                if ($geoindex) {
76
                    $geoindex->delete($model->getKey());
77
                }
78
            } else {
79
                $index->insert($array);
80
            }
81
            if ($geoindex && !empty($array['longitude']) && !empty($array['latitude'])) {
82
                $array['longitude'] = (float) $array['longitude'];
83
                $array['latitude'] = (float) $array['latitude'];
84
                $geoindex->insert($array);
85
            }
86
        });
87
        $index->indexEndTransaction();
88
        if ($this->geotnt) {
89
            $geoindex->indexEndTransaction();
90
        }
91
    }
92
93
    /**
94
     * Remove the given model from the index.
95
     *
96
     * @param Collection $models
97
     *
98
     * @return void
99
     */
100
    public function delete($models)
101
    {
102
        $this->initIndex($models->first());
103
        $models->each(function ($model) {
104
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
105
            $index = $this->tnt->getIndex();
106
            $index->setPrimaryKey($model->getKeyName());
107
            $index->delete($model->getKey());
108
109 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...
110
                $this->geotnt->selectIndex("{$model->searchableAs()}.geoindex");
111
                $index = $this->geotnt->getIndex();
112
                $index->setPrimaryKey($model->getKeyName());
113
                $index->delete($model->getKey());
114
            }
115
        });
116
    }
117
118
    /**
119
     * Perform the given search on the engine.
120
     *
121
     * @param Builder $builder
122
     *
123
     * @return mixed
124
     */
125
    public function search(Builder $builder)
126
    {
127
        try {
128
            return $this->performSearch($builder);
129
        } catch (IndexNotFoundException $e) {
130
            $this->initIndex($builder->model);
131
        }
132
    }
133
134
    /**
135
     * Perform the given search on the engine.
136
     *
137
     * @param Builder $builder
138
     * @param int     $perPage
139
     * @param int     $page
140
     *
141
     * @return mixed
142
     */
143
    public function paginate(Builder $builder, $perPage, $page)
144
    {
145
        $results = $this->performSearch($builder);
146
147
        if ($builder->limit) {
148
            $results['hits'] = $builder->limit;
149
        }
150
151
        $filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
152
153
        $results['hits'] = $filtered->count();
154
155
        $chunks = array_chunk($filtered->toArray(), $perPage);
156
157
        if (empty($chunks)) {
158
            return $results;
159
        }
160
161
        if (array_key_exists($page - 1, $chunks)) {
162
            $results['ids'] = $chunks[$page - 1];
163
        } else {
164
            $results['ids'] = [];
165
        }
166
167
        return $results;
168
    }
169
170
    /**
171
     * Perform the given search on the engine.
172
     *
173
     * @param Builder $builder
174
     *
175
     * @return mixed
176
     */
177
    protected function performSearch(Builder $builder, array $options = [])
178
    {
179
        $index = $builder->index ?: $builder->model->searchableAs();
180
        $limit = $builder->limit ?: 10000;
181
        $this->tnt->selectIndex("{$index}.index");
182
        if ($this->geotnt) {
183
            $this->geotnt->selectIndex("{$index}.geoindex");
184
        }
185
186
        $this->builder = $builder;
187
188
        if (isset($builder->model->asYouType)) {
189
            $this->tnt->asYouType = $builder->model->asYouType;
190
        }
191
192
        if ($builder->callback) {
193
            return call_user_func(
194
                $builder->callback,
195
                $this->tnt,
196
                $builder->query,
197
                $options
198
            );
199
        }
200
201
        if (is_array($builder->query)) {
202
            $location = $builder->query['location'];
203
            $distance = $builder->query['distance'];
204
            $limit = array_key_exists('limit', $builder->query) ? $builder->query['limit'] : 10;
205
            return $this->geotnt->findNearest($location, $distance, $limit);
206
        }
207
208
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
209
            return $this->tnt->searchBoolean($builder->query, $limit);
210
        } else {
211
            return $this->tnt->search($builder->query, $limit);
212
        }
213
    }
214
215
    /**
216
     * Map the given results to instances of the given model.
217
     *
218
     * @param mixed                               $results
219
     * @param \Illuminate\Database\Eloquent\Model $model
220
     *
221
     * @return Collection
222
     */
223
    public function map(Builder $builder, $results, $model)
224
    {
225
        if (is_null($results['ids']) || count($results['ids']) === 0) {
226
            return Collection::make();
227
        }
228
229
        $keys = collect($results['ids'])->values()->all();
230
231
        $builder = $this->getBuilder($model);
232
233
        if ($this->builder->queryCallback) {
234
            call_user_func($this->builder->queryCallback, $builder);
235
        }
236
237
        $models = $builder->whereIn(
238
            $model->getQualifiedKeyName(), $keys
239
        )->get()->keyBy($model->getKeyName());
240
241
        // sort models by user choice
242
        if (!empty($this->builder->orders)) {
243
            return $models->values();
244
        }
245
246
        // sort models by tnt search result set
247
        return collect($results['ids'])->map(function ($hit) use ($models) {
248
            if (isset($models[$hit])) {
249
                return $models[$hit];
250
            }
251
        })->filter()->values();
252
    }
253
254
    /**
255
     * Return query builder either from given constraints, or as
256
     * new query. Add where statements to builder when given.
257
     *
258
     * @param \Illuminate\Database\Eloquent\Model $model
259
     *
260
     * @return Builder
261
     */
262
    public function getBuilder($model)
263
    {
264
        // get query as given constraint or create a new query
265
        $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...
266
267
        $builder = $this->handleSoftDeletes($builder, $model);
268
269
        $builder = $this->applyWheres($builder);
270
271
        $builder = $this->applyOrders($builder);
272
273
        return $builder;
274
    }
275
276
    /**
277
     * Pluck and return the primary keys of the given results.
278
     *
279
     * @param mixed $results
280
     * @return \Illuminate\Support\Collection
281
     */
282
    public function mapIds($results)
283
    {
284
        return collect($results['ids'])->values();
285
    }
286
287
    /**
288
     * Get the total count from a raw result returned by the engine.
289
     *
290
     * @param mixed $results
291
     *
292
     * @return int
293
     */
294
    public function getTotalCount($results)
295
    {
296
        return $results['hits'];
297
    }
298
299
    public function initIndex($model)
300
    {
301
        $indexName = $model->searchableAs();
302
303 View Code Duplication
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
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...
304
            $indexer = $this->tnt->createIndex("$indexName.index");
305
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
306
            $indexer->setPrimaryKey($model->getKeyName());
307
        }
308 View Code Duplication
        if ($this->geotnt && !file_exists($this->tnt->config['storage']."/{$indexName}.geoindex")) {
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...
309
            $indexer = $this->geotnt->getIndex();
310
            $indexer->createIndex("$indexName.geoindex");
311
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
312
            $indexer->setPrimaryKey($model->getKeyName());
313
        }
314
    }
315
316
    /**
317
     * The search index results ($results['ids']) need to be compared against our query
318
     * that contains the constraints.
319
     *
320
     * To get the correct results and counts for the pagination, we remove those ids
321
     * from the search index results that were found by the search but are not part of
322
     * the query ($sub) that is constrained.
323
     *
324
     * This is achieved with self joining the constrained query as subquery and selecting
325
     * the ids which are not matching to anything (i.e., is null).
326
     *
327
     * The constraints usually remove only a small amount of results, which is why the non
328
     * matching results are looked up and removed, instead of returning a collection with
329
     * all the valid results.
330
     */
331
    private function discardIdsFromResultSetByConstraints($builder, $searchResults)
332
    {
333
        $qualifiedKeyName    = $builder->model->getQualifiedKeyName(); // tableName.id
334
        $subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
335
336
        $sub = $this->getBuilder($builder->model)->whereIn(
337
            $qualifiedKeyName, $searchResults
338
        ); // sub query for left join
339
340
        $discardIds = $builder->model->newQuery()
341
            ->select($qualifiedKeyName)
342
            ->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as '. $builder->model->getConnection()->getTablePrefix() .'sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
343
            ->addBinding($sub->getQuery()->getBindings(), 'join')
344
            ->whereIn($qualifiedKeyName, $searchResults)
345
            ->whereNull($subQualifiedKeyName)
346
            ->pluck($builder->model->getKeyName());
347
348
        // returns values of $results['ids'] that are not part of $discardIds
349
        return collect($searchResults)->diff($discardIds);
350
    }
351
352
    /**
353
     * Determine if the given model uses soft deletes.
354
     *
355
     * @param  \Illuminate\Database\Eloquent\Model  $model
356
     * @return bool
357
     */
358
    protected function usesSoftDelete($model)
359
    {
360
        return in_array(SoftDeletes::class, class_uses_recursive($model));
361
    }
362
363
    /**
364
     * Determine if soft delete is active and depending on state return the
365
     * appropriate builder.
366
     *
367
     * @param  Builder  $builder
368
     * @param  \Illuminate\Database\Eloquent\Model  $model
369
     * @return Builder
370
     */
371
    private function handleSoftDeletes($builder, $model)
372
    {
373
        // remove where statement for __soft_deleted when soft delete is not active
374
        // does not show soft deleted items when trait is attached to model and
375
        // config('scout.soft_delete') is false
376
        if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
377
            unset($this->builder->wheres['__soft_deleted']);
378
            return $builder;
379
        }
380
381
        /**
382
         * Use standard behaviour of Laravel Scout builder class to support soft deletes.
383
         *
384
         * When no __soft_deleted statement is given return all entries
385
         */
386
        if (!in_array('__soft_deleted', $this->builder->wheres)) {
387
            return $builder->withTrashed();
388
        }
389
390
        /**
391
         * When __soft_deleted is 1 then return only soft deleted entries
392
         */
393
        if ($this->builder->wheres['__soft_deleted']) {
394
            $builder = $builder->onlyTrashed();
395
        }
396
397
        /**
398
         * Returns all undeleted entries, default behaviour
399
         */
400
        unset($this->builder->wheres['__soft_deleted']);
401
        return $builder;
402
    }
403
404
    /**
405
     * Apply where statements as constraints to the query builder.
406
     *
407
     * @param Builder $builder
408
     * @return \Illuminate\Support\Collection
409
     */
410
    private function applyWheres($builder)
411
    {
412
        // iterate over given where clauses
413
        return collect($this->builder->wheres)->map(function ($value, $key) {
414
            // for reduce function combine key and value into array
415
            return [$key, $value];
416
        })->reduce(function ($builder, $where) {
417
            // separate key, value again
418
            list($key, $value) = $where;
419
            return $builder->where($key, $value);
420
        }, $builder);
421
    }
422
423
    /**
424
     * Apply order by statements as constraints to the query builder.
425
     *
426
     * @param Builder $builder
427
     * @return \Illuminate\Support\Collection
428
     */
429
    private function applyOrders($builder)
430
    {
431
        //iterate over given orderBy clauses - should be only one
432
        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...
433
            // for reduce function combine key and value into array
434
            return [$value["column"], $value["direction"]];
435
        })->reduce(function ($builder, $orderBy) {
436
            // separate key, value again
437
            list($column, $direction) = $orderBy;
438
            return $builder->orderBy($column, $direction);
439
        }, $builder);
440
    }
441
442
    /**
443
     * Flush all of the model's records from the engine.
444
     *
445
     * @param  \Illuminate\Database\Eloquent\Model  $model
446
     * @return void
447
     */
448
    public function flush($model)
449
    {
450
        $indexName   = $model->searchableAs();
451
        $pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
452
        if (file_exists($pathToIndex)) {
453
            unlink($pathToIndex);
454
        }
455
456
        if ($this->geotnt){
457
            $pathToGeoIndex = $this->geotnt->config['storage']."/{$indexName}.geoindex";
458
            if (file_exists($pathToGeoIndex)) {
459
                unlink($pathToGeoIndex);
460
            }
461
        }
462
    }
463
}
464