Passed
Pull Request — master (#155)
by Mozammil
03:51
created

UpdateJob::handle()   B

Complexity

Conditions 10
Paths 33

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10.2918

Importance

Changes 0
Metric Value
cc 10
eloc 28
nc 33
nop 1
dl 0
loc 49
ccs 24
cts 28
cp 0.8571
crap 10.2918
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Jobs;
15
16
use ReflectionClass;
17
use function in_array;
18
use function is_array;
19
use function get_class;
20
use function is_string;
21
use Illuminate\Support\Arr;
22
use Illuminate\Support\Str;
23
use Illuminate\Support\Collection;
24
use Algolia\AlgoliaSearch\SearchClient;
25
use Illuminate\Database\Eloquent\Model;
26
use Illuminate\Database\Eloquent\SoftDeletes;
27
use Algolia\ScoutExtended\Searchable\ModelsResolver;
28
use Algolia\ScoutExtended\Contracts\SplitterContract;
29
use Algolia\ScoutExtended\Searchable\ObjectIdEncrypter;
30
use Algolia\ScoutExtended\Transformers\ConvertDatesToTimestamps;
31
use Algolia\ScoutExtended\Transformers\ConvertNumericStringsToNumbers;
32
33
/**
34
 * @internal
35
 */
36
final class UpdateJob
37
{
38
    /**
39
     * Contains a list of splittables searchables.
40
     *
41
     * Example: [
42
     *      '\App\Thread' => true,
43
     *      '\App\User' => false,
44
     * ];
45
     *
46
     * @var array
47
     */
48
    private $splittables = [];
49
50
    /**
51
     * @var \Illuminate\Support\Collection
52
     */
53
    private $searchables;
54
55
    /**
56
     * Holds the searchables with a declared
57
     * toSearchableArray method.
58
     *
59
     * @var array
60
     */
61
    private $searchablesWithToSearchableArray = [];
62
63
    /**
64
     * Holds a list of transformers to apply by
65
     * default.
66
     *
67
     * @var array
68
     */
69
    private static $transformers = [
70
        ConvertNumericStringsToNumbers::class,
71
        ConvertDatesToTimestamps::class,
72
    ];
73
74
    /**
75
     * UpdateJob constructor.
76
     *
77
     * @param \Illuminate\Support\Collection $searchables
78
     *
79
     * @return void
80
     */
81 17
    public function __construct(Collection $searchables)
82
    {
83 17
        $this->searchables = $searchables;
84 17
    }
85
86
    /**
87
     * @param \Algolia\AlgoliaSearch\SearchClient $client
88
     *
89
     * @return void
90
     */
91 17
    public function handle(SearchClient $client): void
92
    {
93 17
        if ($this->searchables->isEmpty()) {
94
            return;
95
        }
96
97 17
        if (config('scout.soft_delete', false) && $this->usesSoftDelete($this->searchables->first())) {
98
            $this->searchables->each->pushSoftDeleteMetadata();
99
        }
100
101 17
        $index = $client->initIndex($this->searchables->first()->searchableAs());
102
103 17
        $objectsToSave = [];
104 17
        $searchablesToDelete = [];
105
106 17
        foreach ($this->searchables as $key => $searchable) {
107 17
            $metadata = Arr::except($searchable->scoutMetadata(), ModelsResolver::$metadata);
108
109 17
            if (empty($array = array_merge($searchable->toSearchableArray(), $metadata))) {
110
                continue;
111
            }
112
113 17
            if (! $this->hasToSearchableArray($searchable)) {
114 13
                $array = $searchable->getModel()->transform($array);
115
            }
116
117 17
            $array['_tags'] = (array) ($array['_tags'] ?? []);
118
119 17
            $array['_tags'][] = ObjectIdEncrypter::encrypt($searchable);
120
121 17
            if ($this->shouldBeSplitted($searchable)) {
122 5
                $objects = $this->splitSearchable($searchable, $array);
123
124 5
                foreach ($objects as $part => $object) {
125 5
                    $object['objectID'] = ObjectIdEncrypter::encrypt($searchable, (int) $part);
126 5
                    $objectsToSave[] = $object;
127
                }
128 5
                $searchablesToDelete[] = $searchable;
129
            } else {
130 12
                $array['objectID'] = ObjectIdEncrypter::encrypt($searchable);
131 17
                $objectsToSave[] = $array;
132
            }
133
        }
134
135 17
        dispatch_now(new DeleteJob(collect($searchablesToDelete)));
136
137 17
        $result = $index->saveObjects($objectsToSave);
138 17
        if (config('scout.synchronous', false)) {
139
            $result->wait();
140
        }
141 17
    }
142
143
    /**
144
     * @param  object $searchable
145
     *
146
     * @return bool
147
     */
148 17
    private function shouldBeSplitted($searchable): bool
149
    {
150 17
        $class = get_class($searchable->getModel());
151
152 17
        if (! array_key_exists($class, $this->splittables)) {
153 17
            $this->splittables[$class] = false;
154
155 17
            foreach ($searchable->toSearchableArray() as $key => $value) {
156 17
                $method = 'split'.Str::camel($key);
157 17
                $model = $searchable->getModel();
158 17
                if (method_exists($model, $method)) {
159 5
                    $this->splittables[$class] = true;
160 17
                    break;
161
                }
162
            }
163
        }
164
165 17
        return $this->splittables[$class];
166
    }
167
168
    /**
169
     * @param  object $searchable
170
     * @param  array $array
171
     *
172
     * @return array
173
     */
174 5
    private function splitSearchable($searchable, array $array): array
175
    {
176 5
        $pieces = [];
177 5
        foreach ($array as $key => $value) {
178 5
            $method = 'split'.Str::camel((string) $key);
179 5
            $model = $searchable->getModel();
180 5
            if (method_exists($model, $method)) {
181 5
                $result = $model->{$method}($value);
182 5
                $splittedBy = $key;
183 5
                $pieces[$splittedBy] = [];
184
                switch (true) {
185 5
                    case is_array($result):
186 3
                        $pieces[$splittedBy] = $result;
187 3
                        break;
188 3
                    case is_string($result):
189 1
                        $pieces[$splittedBy] = app($result)->split($model, $value);
0 ignored issues
show
Bug introduced by
The method split() does not exist on Illuminate\Foundation\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

189
                        $pieces[$splittedBy] = app($result)->/** @scrutinizer ignore-call */ split($model, $value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
190 1
                        break;
191 2
                    case $result instanceof SplitterContract:
192 2
                        $pieces[$splittedBy] = $result->split($model, $value);
193 5
                        break;
194
                }
195
            }
196
        }
197
198 5
        $objects = [[]];
199 5
        foreach ($pieces as $splittedBy => $values) {
200 5
            $temp = [];
201 5
            foreach ($objects as $object) {
202 5
                foreach ($values as $value) {
203 5
                    $temp[] = array_merge($object, [$splittedBy => $value]);
204
                }
205
            }
206 5
            $objects = $temp;
207
        }
208
209
        return array_map(function ($object) use ($array) {
210 5
            return array_merge($array, $object);
211 5
        }, $objects);
212
    }
213
214
    /**
215
     * Determine if the given searchable uses soft deletes.
216
     *
217
     * @param  object $searchable
218
     *
219
     * @return bool
220
     */
221 1
    private function usesSoftDelete($searchable): bool
222
    {
223 1
        return $searchable instanceof Model && in_array(SoftDeletes::class, class_uses_recursive($searchable), true);
224
    }
225
226
    /**
227
     * @param  object $searchable
228
     *
229
     * @return bool
230
     */
231 17
    private function hasToSearchableArray($searchable): bool
232
    {
233 17
        $searchableClass = get_class($searchable);
234
235 17
        if (! array_key_exists($searchableClass, $this->searchablesWithToSearchableArray)) {
236 17
            $reflectionClass = new ReflectionClass(get_class($searchable));
237
238 17
            $this->searchablesWithToSearchableArray[$searchableClass] =
239 17
                ends_with((string) $reflectionClass->getMethod('toSearchableArray')->getFileName(),
240 17
                    (string) $reflectionClass->getFileName());
241
        }
242
243 17
        return $this->searchablesWithToSearchableArray[$searchableClass];
244
    }
245
246
    /**
247
     * Returns the default update job transformers.
248
     *
249
     * @return array
250
     */
251 13
    public static function getTransformers(): array
252
    {
253 13
        return self::$transformers;
254
    }
255
}
256