Completed
Pull Request — master (#211)
by
unknown
07:20
created

UpdateJob   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 97.8%

Importance

Changes 6
Bugs 2 Features 0
Metric Value
eloc 92
c 6
b 2
f 0
dl 0
loc 232
ccs 89
cts 91
cp 0.978
rs 9.68
wmc 34

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A usesSoftDelete() 0 3 2
A hasToSearchableArray() 0 13 2
B splitSearchable() 0 38 9
C handle() 0 63 15
A getTransformers() 0 3 1
A shouldBeSplitted() 0 18 4
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 21
    public function __construct(Collection $searchables)
82
    {
83 21
        $this->searchables = $searchables;
84 21
    }
85
86
    /**
87
     * @param \Algolia\AlgoliaSearch\SearchClient $client
88
     *
89
     * @return void
90
     */
91 21
    public function handle(SearchClient $client): void
92
    {
93 21
        if ($this->searchables->isEmpty()) {
94
            return;
95
        }
96
97 21
        if (config('scout.soft_delete', false) && $this->usesSoftDelete($this->searchables->first())) {
98 1
            $this->searchables->each->pushSoftDeleteMetadata();
99
        }
100
101 21
        $index = $client->initIndex($this->searchables->first()->searchableAs());
102
103 21
        $objectsToSave = [];
104 21
        $searchablesToDelete = [];
105
106 21
        foreach ($this->searchables as $key => $searchable) {
107 21
            $metadata = Arr::except($searchable->scoutMetadata(), ModelsResolver::$metadata);
108
109 21
            $model = $searchable->getModel();
110 21
            $hasToSearchableRecords = method_exists($model, 'toSearchableRecords');
111
112 21
            if ($hasToSearchableRecords) {
113 1
                $objects = $model->toSearchableRecords();
114
            } else {
115 20
                $objects = [$searchable->toSearchableArray()];
116
            }
117
118 21
            if (empty($objects) || empty($objects[0])) {
119 2
                continue;
120
            }
121
122 20
            foreach ($objects as $part => $object) {
123 20
                $object = array_merge($object, $metadata);
124
125 20
                if (! $this->hasToSearchableArray($searchable)) {
126 15
                    $object = $model->transform($object);
127
                }
128
129 20
                $object['_tags'] = (array) ($object['_tags'] ?? []);
130 20
                $object['_tags'][] = ObjectIdEncrypter::encrypt($searchable);
131 20
                $objects[$part] = $object;
132
            }
133
134 20
            if ($hasToSearchableRecords || $this->shouldBeSplitted($searchable)) {
135 6
                if (!$hasToSearchableRecords) {
136 5
                    $objects = $this->splitSearchable($searchable, $objects[0]);
137
                }
138 6
                foreach ($objects as $part => $object) {
139 6
                    $object['objectID'] = ObjectIdEncrypter::encrypt($searchable, (int) $part);
140 6
                    $objectsToSave[] = $object;
141
                }
142 6
                $searchablesToDelete[] = $searchable;
143
            } else {
144 14
                $objects[0]['objectID'] = ObjectIdEncrypter::encrypt($searchable);
145 20
                $objectsToSave[] = $objects[0];
146
            }
147
        }
148
149 21
        dispatch_now(new DeleteJob(collect($searchablesToDelete)));
150
151 21
        $result = $index->saveObjects($objectsToSave);
152 21
        if (config('scout.synchronous', false)) {
153
            $result->wait();
154
        }
155 21
    }
156
157
    /**
158
     * @param  object $searchable
159
     *
160
     * @return bool
161
     */
162 19
    private function shouldBeSplitted($searchable): bool
163
    {
164 19
        $class = get_class($searchable->getModel());
165
166 19
        if (! array_key_exists($class, $this->splittables)) {
167 19
            $this->splittables[$class] = false;
168
169 19
            foreach ($searchable->toSearchableArray() as $key => $value) {
170 19
                $method = 'split'.Str::camel($key);
171 19
                $model = $searchable->getModel();
172 19
                if (method_exists($model, $method)) {
173 5
                    $this->splittables[$class] = true;
174 19
                    break;
175
                }
176
            }
177
        }
178
179 19
        return $this->splittables[$class];
180
    }
181
182
    /**
183
     * @param  object $searchable
184
     * @param  array $array
185
     *
186
     * @return array
187
     */
188 5
    private function splitSearchable($searchable, array $array): array
189
    {
190 5
        $pieces = [];
191 5
        foreach ($array as $key => $value) {
192 5
            $method = 'split'.Str::camel((string) $key);
193 5
            $model = $searchable->getModel();
194 5
            if (method_exists($model, $method)) {
195 5
                $result = $model->{$method}($value);
196 5
                $splittedBy = $key;
197 5
                $pieces[$splittedBy] = [];
198
                switch (true) {
199 5
                    case is_array($result):
200 3
                        $pieces[$splittedBy] = $result;
201 3
                        break;
202 3
                    case is_string($result):
203 1
                        $pieces[$splittedBy] = app($result)->split($model, $value);
0 ignored issues
show
Bug introduced by
The method split() does not exist on Illuminate\Contracts\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

203
                        $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...
204 1
                        break;
205 2
                    case $result instanceof SplitterContract:
206 2
                        $pieces[$splittedBy] = $result->split($model, $value);
207 5
                        break;
208
                }
209
            }
210
        }
211
212 5
        $objects = [[]];
213 5
        foreach ($pieces as $splittedBy => $values) {
214 5
            $temp = [];
215 5
            foreach ($objects as $object) {
216 5
                foreach ($values as $value) {
217 5
                    $temp[] = array_merge($object, [$splittedBy => $value]);
218
                }
219
            }
220 5
            $objects = $temp;
221
        }
222
223
        return array_map(function ($object) use ($array) {
224 5
            return array_merge($array, $object);
225 5
        }, $objects);
226
    }
227
228
    /**
229
     * Determine if the given searchable uses soft deletes.
230
     *
231
     * @param  object $searchable
232
     *
233
     * @return bool
234
     */
235 2
    private function usesSoftDelete($searchable): bool
236
    {
237 2
        return $searchable instanceof Model && in_array(SoftDeletes::class, class_uses_recursive($searchable), true);
238
    }
239
240
    /**
241
     * @param  object $searchable
242
     *
243
     * @return bool
244
     */
245 20
    private function hasToSearchableArray($searchable): bool
246
    {
247 20
        $searchableClass = get_class($searchable);
248
249 20
        if (! array_key_exists($searchableClass, $this->searchablesWithToSearchableArray)) {
250 20
            $reflectionClass = new ReflectionClass(get_class($searchable));
251
252 20
            $this->searchablesWithToSearchableArray[$searchableClass] =
253 20
                Str::endsWith((string) $reflectionClass->getMethod('toSearchableArray')->getFileName(),
254 20
                    (string) $reflectionClass->getFileName());
255
        }
256
257 20
        return $this->searchablesWithToSearchableArray[$searchableClass];
258
    }
259
260
    /**
261
     * Returns the default update job transformers.
262
     *
263
     * @return array
264
     */
265 15
    public static function getTransformers(): array
266
    {
267 15
        return self::$transformers;
268
    }
269
}
270