Issues (18)

src/Jobs/UpdateJob.php (1 issue)

Labels
Severity
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 Algolia\AlgoliaSearch\SearchClient;
17
use Algolia\ScoutExtended\Contracts\SplitterContract;
18
use Algolia\ScoutExtended\Searchable\ModelsResolver;
19
use Algolia\ScoutExtended\Searchable\ObjectIdEncrypter;
20
use Algolia\ScoutExtended\Transformers\ConvertDatesToTimestamps;
21
use Algolia\ScoutExtended\Transformers\ConvertNumericStringsToNumbers;
22
use function get_class;
23
use Illuminate\Database\Eloquent\Model;
24
use Illuminate\Database\Eloquent\SoftDeletes;
25
use Illuminate\Support\Arr;
26
use Illuminate\Support\Collection;
27
use Illuminate\Support\Str;
28
use function in_array;
29
use function is_array;
30
use function is_string;
31
use ReflectionClass;
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 20
    public function __construct(Collection $searchables)
82
    {
83 20
        $this->searchables = $searchables;
84 20
    }
85
86
    /**
87
     * @param \Algolia\AlgoliaSearch\SearchClient $client
88
     *
89
     * @return void
90
     */
91 20
    public function handle(SearchClient $client): void
92
    {
93 20
        if ($this->searchables->isEmpty()) {
94
            return;
95
        }
96
97 20
        if (config('scout.soft_delete', false) && $this->usesSoftDelete($this->searchables->first())) {
98 1
            $this->searchables->each->pushSoftDeleteMetadata();
99
        }
100
101 20
        $index = $client->initIndex($this->searchables->first()->searchableAs());
102
103 20
        $objectsToSave = [];
104 20
        $searchablesToDelete = [];
105
106 20
        foreach ($this->searchables as $key => $searchable) {
107 20
            $metadata = Arr::except($searchable->scoutMetadata(), ModelsResolver::$metadata);
108
109 20
            if (empty($searchableArray = $searchable->toSearchableArray())) {
110 2
                continue;
111
            }
112
113 19
            $array = array_merge($searchableArray, $metadata);
114
115 19
            if (! $this->hasToSearchableArray($searchable)) {
116 14
                $array = $searchable->getModel()->transform($array);
117
            }
118
119 19
            $array['_tags'] = (array) ($array['_tags'] ?? []);
120
121 19
            $array['_tags'][] = ObjectIdEncrypter::encrypt($searchable);
122
123 19
            if ($this->shouldBeSplitted($searchable)) {
124 5
                $objects = $this->splitSearchable($searchable, $array);
125
126 5
                foreach ($objects as $part => $object) {
127 5
                    $object['objectID'] = ObjectIdEncrypter::encrypt($searchable, (int) $part);
128 5
                    $objectsToSave[] = $object;
129
                }
130 5
                $searchablesToDelete[] = $searchable;
131
            } else {
132 14
                $array['objectID'] = ObjectIdEncrypter::encrypt($searchable);
133 19
                $objectsToSave[] = $array;
134
            }
135
        }
136
137 20
        dispatch_now(new DeleteJob(collect($searchablesToDelete)));
138
139 20
        $result = $index->saveObjects($objectsToSave);
140 20
        if (config('scout.synchronous', false)) {
141
            $result->wait();
142
        }
143 20
    }
144
145
    /**
146
     * @param  object $searchable
147
     *
148
     * @return bool
149
     */
150 19
    private function shouldBeSplitted($searchable): bool
151
    {
152 19
        $class = get_class($searchable->getModel());
153
154 19
        if (! array_key_exists($class, $this->splittables)) {
155 19
            $this->splittables[$class] = false;
156
157 19
            foreach ($searchable->toSearchableArray() as $key => $value) {
158 19
                $method = 'split'.Str::camel($key);
159 19
                $model = $searchable->getModel();
160 19
                if (method_exists($model, $method)) {
161 5
                    $this->splittables[$class] = true;
162 19
                    break;
163
                }
164
            }
165
        }
166
167 19
        return $this->splittables[$class];
168
    }
169
170
    /**
171
     * @param  object $searchable
172
     * @param  array $array
173
     *
174
     * @return array
175
     */
176 5
    private function splitSearchable($searchable, array $array): array
177
    {
178 5
        $pieces = [];
179 5
        foreach ($array as $key => $value) {
180 5
            $method = 'split'.Str::camel((string) $key);
181 5
            $model = $searchable->getModel();
182 5
            if (method_exists($model, $method)) {
183 5
                $result = $model->{$method}($value);
184 5
                $splittedBy = $key;
185 5
                $pieces[$splittedBy] = [];
186
                switch (true) {
187 5
                    case is_array($result):
188 3
                        $pieces[$splittedBy] = $result;
189 3
                        break;
190 3
                    case is_string($result):
191 1
                        $pieces[$splittedBy] = app($result)->split($model, $value);
0 ignored issues
show
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

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