Completed
Pull Request — feat/html-splitter (#176)
by Nuno
20:20 queued 15:59
created

UpdateJob   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Test Coverage

Coverage 94.32%

Importance

Changes 0
Metric Value
eloc 88
dl 0
loc 231
ccs 83
cts 88
cp 0.9432
rs 9.84
c 0
b 0
f 0
wmc 32

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
B handle() 0 49 10
A shouldBeSplitted() 0 18 4
A hasToSearchableArray() 0 11 2
A usesSoftDelete() 0 3 2
C splitSearchable() 0 51 12
A getTransformers() 0 3 1
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 19
    public function __construct(Collection $searchables)
82
    {
83 19
        $this->searchables = $searchables;
84 19
    }
85
86
    /**
87
     * @param \Algolia\AlgoliaSearch\SearchClient $client
88
     *
89
     * @return void
90
     */
91 19
    public function handle(SearchClient $client): void
92
    {
93 19
        if ($this->searchables->isEmpty()) {
94
            return;
95
        }
96
97 19
        if (config('scout.soft_delete', false) && $this->usesSoftDelete($this->searchables->first())) {
98
            $this->searchables->each->pushSoftDeleteMetadata();
99
        }
100
101 19
        $index = $client->initIndex($this->searchables->first()->searchableAs());
102
103 19
        $objectsToSave = [];
104 19
        $searchablesToDelete = [];
105
106 19
        foreach ($this->searchables as $key => $searchable) {
107 19
            $metadata = Arr::except($searchable->scoutMetadata(), ModelsResolver::$metadata);
108
109 19
            if (empty($array = array_merge($searchable->toSearchableArray(), $metadata))) {
110
                continue;
111
            }
112
113 19
            if (! $this->hasToSearchableArray($searchable)) {
114 14
                $array = $searchable->getModel()->transform($array);
115
            }
116
117 19
            $array['_tags'] = (array) ($array['_tags'] ?? []);
118
119 19
            $array['_tags'][] = ObjectIdEncrypter::encrypt($searchable);
120
121 19
            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 14
                $array['objectID'] = ObjectIdEncrypter::encrypt($searchable);
131 19
                $objectsToSave[] = $array;
132
            }
133
        }
134
135 19
        dispatch_now(new DeleteJob(collect($searchablesToDelete)));
136
137 19
        $result = $index->saveObjects($objectsToSave);
138 19
        if (config('scout.synchronous', false)) {
139
            $result->wait();
140
        }
141 19
    }
142
143
    /**
144
     * @param  object $searchable
145
     *
146
     * @return bool
147
     */
148 19
    private function shouldBeSplitted($searchable): bool
149
    {
150 19
        $class = get_class($searchable->getModel());
151
152 19
        if (! array_key_exists($class, $this->splittables)) {
153 19
            $this->splittables[$class] = false;
154
155 19
            foreach ($searchable->toSearchableArray() as $key => $value) {
156 19
                $method = 'split'.Str::camel($key);
157 19
                $model = $searchable->getModel();
158 19
                if (method_exists($model, $method)) {
159 5
                    $this->splittables[$class] = true;
160 19
                    break;
161
                }
162
            }
163
        }
164
165 19
        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
        $model = $searchable->getModel();
178
179 5
        foreach ($array as $key => $value) {
180 5
            $method = 'split'.Str::camel((string) $key);
181
182 5
            if (method_exists($model, $method)) {
183 5
                $result = $model->{$method}($value);
184 5
                $splittedBy = $key;
185
186 5
                $pieces[$splittedBy] = [];
187
                switch (true) {
188 5
                    case is_array($result):
189 3
                        $pieces[$splittedBy] = $result;
190 3
                        break;
191 3
                    case is_string($result):
192 1
                        $pieces = 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

192
                        $pieces = 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...
193 1
                        break;
194 2
                    case $result instanceof SplitterContract:
195 2
                        $pieces = $result->split($model, $value);
196 5
                        break;
197
                }
198
            }
199
        }
200
201 5
        if (! empty($result)) {
202 5
            if (is_array($result)) {
203 2
                $objects = [[]];
204 2
                foreach ($pieces as $splittedBy => $values) {
205 2
                    $temp = [];
206 2
                    foreach ($objects as $object) {
207 2
                        foreach ($values as $value) {
208 2
                            $temp[] = array_merge($object, [$splittedBy => $value]);
209
                        }
210
                    }
211 2
                    $objects = $temp;
212
                }
213
214
                return array_map(function ($object) use ($array) {
215 2
                    return array_merge($array, $object);
216 2
                }, $objects);
217
            } else {
218 3
                $objects = [];
219 3
                unset($array['body']);
220 3
                foreach ($pieces as $piece) {
221 3
                    $objects[] = array_merge($piece, $array);
222
                }
223
224 3
                return $objects;
225
            }
226
        }
0 ignored issues
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 201 is false. This is incompatible with the type-hinted return array. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
227
    }
228
229
    /**
230
     * Determine if the given searchable uses soft deletes.
231
     *
232
     * @param  object $searchable
233
     *
234
     * @return bool
235
     */
236 1
    private function usesSoftDelete($searchable): bool
237
    {
238 1
        return $searchable instanceof Model && in_array(SoftDeletes::class, class_uses_recursive($searchable), true);
239
    }
240
241
    /**
242
     * @param  object $searchable
243
     *
244
     * @return bool
245
     */
246 19
    private function hasToSearchableArray($searchable): bool
247
    {
248 19
        $searchableClass = get_class($searchable);
249
250 19
        if (! array_key_exists($searchableClass, $this->searchablesWithToSearchableArray)) {
251 19
            $reflectionClass = new ReflectionClass(get_class($searchable));
252
253 19
            $this->searchablesWithToSearchableArray[$searchableClass] = ends_with((string) $reflectionClass->getMethod('toSearchableArray')->getFileName(), (string) $reflectionClass->getFileName());
0 ignored issues
show
Deprecated Code introduced by
The function ends_with() has been deprecated: Str::endsWith() should be used directly instead. Will be removed in Laravel 5.9. ( Ignorable by Annotation )

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

253
            $this->searchablesWithToSearchableArray[$searchableClass] = /** @scrutinizer ignore-deprecated */ ends_with((string) $reflectionClass->getMethod('toSearchableArray')->getFileName(), (string) $reflectionClass->getFileName());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
254
        }
255
256 19
        return $this->searchablesWithToSearchableArray[$searchableClass];
257
    }
258
259
    /**
260
     * Returns the default update job transformers.
261
     *
262
     * @return array
263
     */
264 14
    public static function getTransformers(): array
265
    {
266 14
        return self::$transformers;
267
    }
268
}
269