Completed
Push — 5.1 ( c7eca6...1f8916 )
by Jarek
05:49
created

Builder::search()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 8.8571
cc 6
eloc 9
nc 8
nop 4
1
<?php
2
3
namespace Sofa\Eloquence;
4
5
use Sofa\Eloquence\Searchable\Column;
6
use Illuminate\Database\Query\Expression;
7
use Sofa\Hookable\Builder as HookableBuilder;
8
use Sofa\Eloquence\Searchable\ColumnCollection;
9
use Sofa\Eloquence\Contracts\Relations\JoinerFactory;
10
use Sofa\Eloquence\Contracts\Searchable\ParserFactory;
11
use Illuminate\Database\Query\Grammars\PostgresGrammar;
12
use Sofa\Eloquence\Searchable\Subquery as SearchableSubquery;
13
14
/**
15
 * @method $this leftJoin($table, $one, $operator, $two)
16
 */
17
class Builder extends HookableBuilder
18
{
19
    /**
20
     * Parser factory instance.
21
     *
22
     * @var \Sofa\Eloquence\Contracts\Searchable\ParserFactory
23
     */
24
    protected static $parser;
25
26
    /**
27
     * Joiner factory instance.
28
     *
29
     * @var \Sofa\Eloquence\Contracts\Relations\JoinerFactory
30
     */
31
    protected static $joinerFactory;
32
33
    /**
34
     * Relations joiner instance.
35
     *
36
     * @var \Sofa\Eloquence\Contracts\Relations\Joiner
37
     */
38
    protected $joiner;
39
40
    /*
41
    |--------------------------------------------------------------------------
42
    | Additional features
43
    |--------------------------------------------------------------------------
44
    */
45
46
    /**
47
     * Execute the query as a "select" statement.
48
     *
49
     * @param  array $columns
50
     * @return \Illuminate\Database\Eloquent\Collection
51
     */
52
    public function get($columns = ['*'])
53
    {
54
        if ($this->query->from instanceof Subquery) {
55
            $this->wheresToSubquery($this->query->from);
56
        }
57
58
        return parent::get($columns);
59
    }
60
61
    /**
62
     * Search through any columns on current table or any defined relations
63
     * and return results ordered by search relevance.
64
     *
65
     * @param  array|string $query
66
     * @param  array $columns
67
     * @param  boolean $fulltext
68
     * @param  float $threshold
69
     * @return $this
70
     */
71
    public function search($query, $columns = null, $fulltext = true, $threshold = null)
72
    {
73
        if (is_bool($columns)) {
74
            list($fulltext, $columns) = [$columns, []];
75
        }
76
77
        $parser = static::$parser->make();
78
79
        $words = is_array($query) ? $query : $parser->parseQuery($query, $fulltext);
80
81
        $columns = $parser->parseWeights($columns ?: $this->model->getSearchableColumns());
82
83
        if (count($words) && count($columns)) {
84
            $this->query->from($this->buildSubquery($words, $columns, $threshold));
85
        }
86
87
        return $this;
88
    }
89
90
    /**
91
     * Build the search subquery.
92
     *
93
     * @param  array $words
94
     * @param  array $mappings
95
     * @param  float $threshold
96
     * @return \Sofa\Eloquence\Searchable\Subquery
97
     */
98
    protected function buildSubquery(array $words, array $mappings, $threshold)
99
    {
100
        $subquery = new SearchableSubquery($this->query->newQuery(), $this->model->getTable());
101
102
        $columns = $this->joinForSearch($mappings, $subquery);
103
104
        $threshold = (is_null($threshold))
105
                        ? array_sum($columns->getWeights()) / 4
106
                        : (float) $threshold;
107
108
        $subquery->select($this->model->getTable() . '.*')
0 ignored issues
show
Documentation Bug introduced by
The method select does not exist on object<Sofa\Eloquence\Searchable\Subquery>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
109
                 ->from($this->model->getTable())
110
                 ->groupBy($this->model->getQualifiedKeyName());
111
112
        $this->addSearchClauses($subquery, $columns, $words, $threshold);
113
114
        return $subquery;
115
    }
116
117
    /**
118
     * Add select and where clauses on the subquery.
119
     *
120
     * @param  \Sofa\Eloquence\Searchable\Subquery $subquery
121
     * @param  \Sofa\Eloquence\Searchable\ColumnCollection $columns
122
     * @param  array $words
123
     * @param  float $threshold
124
     * @return void
125
     */
126
    protected function addSearchClauses(
127
        SearchableSubquery $subquery,
128
        ColumnCollection $columns,
129
        array $words,
130
        $threshold
131
    ) {
132
        $whereBindings = $this->searchSelect($subquery, $columns, $words, $threshold);
0 ignored issues
show
Unused Code introduced by
The call to Builder::searchSelect() has too many arguments starting with $threshold.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
133
134
        // For morphOne/morphMany support we need to port the bindings from JoinClauses.
135
        $joinBindings = array_flatten(array_pluck((array) $subquery->getQuery()->joins, 'bindings'));
136
137
        $this->addBinding($joinBindings, 'select');
0 ignored issues
show
Documentation Bug introduced by
The method addBinding does not exist on object<Sofa\Eloquence\Builder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
138
139
        // Developer may want to skip the score threshold filtering by passing zero
140
        // value as threshold in order to simply order full result by relevance.
141
        // Otherwise we are going to add where clauses for speed improvement.
142
        if ($threshold > 0) {
143
            $this->searchWhere($subquery, $columns, $words, $whereBindings);
144
        }
145
146
        $this->query->where('relevance', '>=', new Expression($threshold));
147
148
        $this->query->orders = array_merge(
149
            [['column' => 'relevance', 'direction' => 'desc']],
150
            (array) $this->query->orders
151
        );
152
    }
153
154
    /**
155
     * Apply relevance select on the subquery.
156
     *
157
     * @param  \Sofa\Eloquence\Searchable\Subquery $subquery
158
     * @param  \Sofa\Eloquence\Searchable\ColumnCollection $columns
159
     * @param  array $words
160
     * @return array
161
     */
162
    protected function searchSelect(SearchableSubquery $subquery, ColumnCollection $columns, array $words)
163
    {
164
        $cases = $bindings = [];
165
166
        foreach ($columns as $column) {
167
            list($cases[], $binding) = $this->buildCase($column, $words);
168
169
            $bindings = array_merge_recursive($bindings, $binding);
170
        }
171
172
        $select = implode(' + ', $cases);
173
174
        $subquery->selectRaw("max({$select}) as relevance");
0 ignored issues
show
Documentation Bug introduced by
The method selectRaw does not exist on object<Sofa\Eloquence\Searchable\Subquery>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
175
176
        $this->addBinding($bindings['select'], 'select');
0 ignored issues
show
Documentation Bug introduced by
The method addBinding does not exist on object<Sofa\Eloquence\Builder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
177
178
        return $bindings['where'];
179
    }
180
181
    /**
182
     * Apply where clauses on the subquery.
183
     *
184
     * @param  \Sofa\Eloquence\Searchable\Subquery $subquery
185
     * @param  \Sofa\Eloquence\Searchable\ColumnCollection $columns
186
     * @param  array $words
187
     * @return void
188
     */
189
    protected function searchWhere(
190
        SearchableSubquery $subquery,
191
        ColumnCollection $columns,
192
        array $words,
193
        array $bindings
194
    ) {
195
        $operator = $this->getLikeOperator();
196
197
        $wheres = [];
198
199
        foreach ($columns as $column) {
200
            $wheres[] = implode(
201
                ' or ',
202
                array_fill(0, count($words), sprintf('%s %s ?', $column->getWrapped(), $operator))
203
            );
204
        }
205
206
        $where = implode(' or ', $wheres);
207
208
        $subquery->whereRaw("({$where})");
0 ignored issues
show
Documentation Bug introduced by
The method whereRaw does not exist on object<Sofa\Eloquence\Searchable\Subquery>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
209
210
        $this->addBinding($bindings, 'select');
0 ignored issues
show
Documentation Bug introduced by
The method addBinding does not exist on object<Sofa\Eloquence\Builder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
211
    }
212
213
    /**
214
     * Move where clauses to subquery to improve performance.
215
     *
216
     * @param  \Sofa\Eloquence\Searchable\Subquery $subquery
217
     * @return void
218
     */
219
    protected function wheresToSubquery(SearchableSubquery $subquery)
220
    {
221
        $bindingKey = 0;
222
223
        $typesToMove = [
224
            'basic', 'in', 'notin', 'between', 'null',
225
            'notnull', 'date', 'day', 'month', 'year',
226
        ];
227
228
        // Here we are going to move all the where clauses that we might apply
229
        // on the subquery in order to improve performance, since this way
230
        // we can drastically reduce number of joined rows on subquery.
231
        foreach ((array) $this->query->wheres as $key => $where) {
232
            $type = strtolower($where['type']);
233
234
            $bindingsCount = $this->countBindings($where, $type);
235
236
            if (in_array($type, $typesToMove) && $this->model->hasColumn($where['column'])) {
237
                unset($this->query->wheres[$key]);
238
239
                $where['column'] = $this->model->getTable() . '.' . $where['column'];
240
241
                $subquery->getQuery()->wheres[] = $where;
242
243
                $whereBindings = $this->query->getRawBindings()['where'];
244
245
                $bindings = array_splice($whereBindings, $bindingKey, $bindingsCount);
246
247
                $this->query->setBindings($whereBindings, 'where');
248
249
                $this->query->addBinding($bindings, 'select');
250
251
            // if where is not to be moved onto the subquery, let's increment
252
            // binding key appropriately, so we can reliably move binding
253
            // for the next where clauses in the loop that is running.
254
            } else {
255
                $bindingKey += $bindingsCount;
256
            }
257
        }
258
    }
259
260
    /**
261
     * Get number of bindings provided for a where clause.
262
     *
263
     * @param  array   $where
264
     * @param  string  $type
265
     * @return integer
266
     */
267
    protected function countBindings(array $where, $type)
268
    {
269
        if ($this->isHasWhere($where, $type)) {
270
            return substr_count($where['column'] . $where['value'], '?');
271
272
        } elseif ($type === 'basic') {
273
            return (int) !$where['value'] instanceof Expression;
274
275
        } elseif (in_array($type, ['basic', 'date', 'year', 'month', 'day'])) {
276
            return (int) !$where['value'] instanceof Expression;
277
278
        } elseif (in_array($type, ['null', 'notnull'])) {
279
            return 0;
280
281
        } elseif ($type === 'between') {
282
            return 2;
283
284
        } elseif (in_array($type, ['in', 'notin'])) {
285
            return count($where['values']);
286
287
        } elseif ($type === 'raw') {
288
            return substr_count($where['sql'], '?');
289
290
        } elseif (in_array($type, ['nested', 'sub', 'exists', 'notexists', 'insub', 'notinsub'])) {
291
            return count($where['query']->getBindings());
292
        }
293
    }
294
295
    /**
296
     * Determine whether where clause is eloquent has subquery.
297
     *
298
     * @param  array  $where
299
     * @param  string $type
300
     * @return boolean
301
     */
302
    protected function isHasWhere($where, $type)
303
    {
304
        return $type === 'basic'
305
                && $where['column'] instanceof Expression
306
                && $where['value'] instanceof Expression;
307
    }
308
309
    /**
310
     * Build case clause from all words for a single column.
311
     *
312
     * @param  \Sofa\Eloquence\Searchable\Column $column
313
     * @param  array  $words
314
     * @return array
315
     */
316
    protected function buildCase(Column $column, array $words)
317
    {
318
        // THIS IS BAD
319
        // @todo refactor
320
321
        $operator = $this->getLikeOperator();
322
323
        $bindings['select'] = $bindings['where'] = array_map(function ($word) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
$bindings was never initialized. Although not strictly required by PHP, it is generally a good practice to add $bindings = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
324
            return $this->caseBinding($word);
325
        }, $words);
326
327
        $case = $this->buildEqualsCase($column, $words);
328
329
        if (strpos(implode('', $words), '*') !== false) {
330
            $leftMatching = [];
331
332 View Code Duplication
            foreach ($words as $key => $word) {
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...
333
                if ($this->isLeftMatching($word)) {
334
                    $leftMatching[] = sprintf('%s %s ?', $column->getWrapped(), $operator);
335
                    $bindings['select'][] = $bindings['where'][$key] = $this->caseBinding($word) . '%';
336
                }
337
            }
338
339 View Code Duplication
            if (count($leftMatching)) {
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...
340
                $leftMatching = implode(' or ', $leftMatching);
341
                $score = 5 * $column->getWeight();
342
                $case .= " + case when {$leftMatching} then {$score} else 0 end";
343
            }
344
345
            $wildcards = [];
346
347 View Code Duplication
            foreach ($words as $key => $word) {
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...
348
                if ($this->isWildcard($word)) {
349
                    $wildcards[] = sprintf('%s %s ?', $column->getWrapped(), $operator);
350
                    $bindings['select'][] = $bindings['where'][$key] = '%'.$this->caseBinding($word) . '%';
351
                }
352
            }
353
354 View Code Duplication
            if (count($wildcards)) {
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...
355
                $wildcards = implode(' or ', $wildcards);
356
                $score = 1 * $column->getWeight();
357
                $case .= " + case when {$wildcards} then {$score} else 0 end";
358
            }
359
        }
360
361
        return [$case, $bindings];
362
    }
363
364
    /**
365
     * Replace '?' with single character SQL wildcards.
366
     *
367
     * @param  string $word
368
     * @return string
369
     */
370
    protected function caseBinding($word)
371
    {
372
        $parser = static::$parser->make();
373
374
        return str_replace('?', '_', $parser->stripWildcards($word));
375
    }
376
377
    /**
378
     * Build basic search case for 'equals' comparison.
379
     *
380
     * @param  \Sofa\Eloquence\Searchable\Column $column
381
     * @param  array  $words
382
     * @return string
383
     */
384
    protected function buildEqualsCase(Column $column, array $words)
385
    {
386
        $equals = implode(' or ', array_fill(0, count($words), sprintf('%s = ?', $column->getWrapped())));
387
388
        $score = 15 * $column->getWeight();
389
390
        return "case when {$equals} then {$score} else 0 end";
391
    }
392
393
    /**
394
     * Determine whether word ends with wildcard.
395
     *
396
     * @param  string  $word
397
     * @return boolean
398
     */
399
    protected function isLeftMatching($word)
400
    {
401
        return ends_with($word, '*');
402
    }
403
404
    /**
405
     * Determine whether word starts and ends with wildcards.
406
     *
407
     * @param  string  $word
408
     * @return boolean
409
     */
410
    protected function isWildcard($word)
411
    {
412
        return ends_with($word, '*') && starts_with($word, '*');
413
    }
414
415
    /**
416
     * Get driver-specific case insensitive like operator.
417
     *
418
     * @return string
419
     */
420
    public function getLikeOperator()
421
    {
422
        $grammar = $this->query->getGrammar();
423
424
        if ($grammar instanceof PostgresGrammar) {
425
            return 'ilike';
426
        }
427
428
        return 'like';
429
    }
430
431
    /**
432
     * Join related tables on the search subquery.
433
     *
434
     * @param  array $mappings
435
     * @param  \Sofa\Eloquence\Searchable\Subquery $subquery
436
     * @return \Sofa\Eloquence\Searchable\ColumnCollection
437
     */
438
    protected function joinForSearch($mappings, $subquery)
439
    {
440
        $mappings = is_array($mappings) ? $mappings : (array) $mappings;
441
442
        $columns = new ColumnCollection;
443
444
        $grammar = $this->query->getGrammar();
445
446
        $joiner = static::$joinerFactory->make($subquery->getQuery(), $this->model);
447
448
        // Here we loop through the search mappings in order to join related tables
449
        // appropriately and build a searchable column collection, which we will
450
        // use to build select and where clauses with correct table prefixes.
451
        foreach ($mappings as $mapping => $weight) {
452
            if (strpos($mapping, '.') !== false) {
453
                list($relation, $column) = $this->model->parseMappedColumn($mapping);
454
455
                $related = $joiner->leftJoin($relation);
456
457
                $columns->add(
458
                    new Column($grammar, $related->getTable(), $column, $mapping, $weight)
459
                );
460
461
            } else {
462
                $columns->add(
463
                    new Column($grammar, $this->model->getTable(), $mapping, $mapping, $weight)
464
                );
465
            }
466
        }
467
468
        return $columns;
469
    }
470
471
    /**
472
     * Prefix selected columns with table name in order to avoid collisions.
473
     *
474
     * @return $this
475
     */
476
    public function prefixColumnsForJoin()
477
    {
478
        if (!$columns = $this->query->columns) {
479
            return $this->select($this->model->getTable() . '.*');
480
        }
481
482
        foreach ($columns as $key => $column) {
483
            if ($this->model->hasColumn($column)) {
484
                $columns[$key] = $this->model->getTable() . '.' . $column;
485
            }
486
        }
487
488
        $this->query->columns = $columns;
489
490
        return $this;
491
    }
492
493
    /**
494
     * Join related tables.
495
     *
496
     * @param  array|string $relations
497
     * @param  string $type
498
     * @return $this
499
     */
500
    public function joinRelations($relations, $type = 'inner')
501
    {
502
        if (is_null($this->joiner)) {
503
            $this->joiner = static::$joinerFactory->make($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<Sofa\Eloquence\Builder>, but the function expects a object<Illuminate\Database\Query\Builder>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
504
        }
505
506
        if (!is_array($relations)) {
507
            list($relations, $type) = [func_get_args(), 'inner'];
508
        }
509
510
        foreach ($relations as $relation) {
511
            $this->joiner->join($relation, $type);
512
        }
513
514
        return $this;
515
    }
516
517
    /**
518
     * Left join related tables.
519
     *
520
     * @param  array|string $relations
521
     * @return $this
522
     */
523
    public function leftJoinRelations($relations)
524
    {
525
        $relations = is_array($relations) ? $relations : func_get_args();
526
527
        return $this->joinRelations($relations, 'left');
528
    }
529
530
    /**
531
     * Right join related tables.
532
     *
533
     * @param  array|string $relations
534
     * @return $this
535
     */
536
    public function rightJoinRelations($relations)
537
    {
538
        $relations = is_array($relations) ? $relations : func_get_args();
539
540
        return $this->joinRelations($relations, 'right');
541
    }
542
543
    /**
544
     * Set search query parser factory instance.
545
     *
546
     * @param \Sofa\Eloquence\Contracts\Searchable\ParserFactory $factory
547
     */
548
    public static function setParserFactory(ParserFactory $factory)
549
    {
550
        static::$parser = $factory;
551
    }
552
553
    /**
554
     * Set the relations joiner factory instance.
555
     *
556
     * @param \Sofa\Eloquence\Contracts\Relations\JoinerFactory $factory
557
     */
558
    public static function setJoinerFactory(JoinerFactory $factory)
559
    {
560
        static::$joinerFactory = $factory;
561
    }
562
}
563