Passed
Push — analysis-8PdMWW ( 07bfe3 )
by Nuno
08:50 queued 01:13
created

UpdateJob::mutateArray()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.288

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 7
nop 2
dl 0
loc 23
ccs 8
cts 10
cp 0.8
crap 6.288
rs 9.2222
c 0
b 0
f 0
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 function in_array;
17
use function is_array;
18
use function get_class;
19
use function is_object;
20
use function is_string;
21
use Illuminate\Support\Str;
22
use Illuminate\Support\Collection;
23
use Algolia\AlgoliaSearch\SearchClient;
24
use Illuminate\Database\Eloquent\Model;
25
use Illuminate\Database\Eloquent\SoftDeletes;
26
use Algolia\ScoutExtended\Searchable\ObjectIdEncrypter;
27
28
/**
29
 * @internal
30
 */
31
final class UpdateJob
32
{
33
    /**
34
     * Contains a list of splittables searchables.
35
     *
36
     * Example: [
37
     *      '\App\Thread' => true,
38
     *      '\App\User' => false,
39
     * ];
40
     *
41
     * @var array
42
     */
43
    private $splittables = [];
44
45
    /**
46
     * @var \Illuminate\Support\Collection
47
     */
48
    private $searchables;
49
50
    /**
51
     * UpdateJob constructor.
52
     *
53
     * @param \Illuminate\Support\Collection $searchables
54
     *
55
     * @return void
56
     */
57 13
    public function __construct(Collection $searchables)
58
    {
59 13
        $this->searchables = $searchables;
60 13
    }
61
62
    /**
63
     * @param \Algolia\AlgoliaSearch\SearchClient $client
64
     *
65
     * @return void
66
     */
67 13
    public function handle(SearchClient $client): void
68
    {
69 13
        if ($this->searchables->isEmpty()) {
70
            return;
71
        }
72
73 13
        if (config('scout.soft_delete', false) && $this->usesSoftDelete($this->searchables->first())) {
74
            $this->searchables->each->pushSoftDeleteMetadata();
75
        }
76
77 13
        $index = $client->initIndex($this->searchables->first()->searchableAs());
78
79 13
        $objectsToSave = [];
80 13
        $searchablesToDelete = [];
81
82 13
        foreach ($this->searchables as $key => $searchable) {
83 13
            if (empty($array = array_merge($searchable->toSearchableArray(), $searchable->scoutMetadata()))) {
84
                continue;
85
            }
86
87 13
            $array = $this->mutateArray($searchable, $array);
88
89 13
            $array['_tags'] = (array) ($array['_tags'] ?? []);
90
91 13
            array_push($array['_tags'], ObjectIdEncrypter::encrypt($searchable));
92
93 13
            if ($this->shouldBeSplitted($searchable)) {
94 5
                $objects = $this->splitSearchable($searchable, $array);
95
96 5
                foreach ($objects as $part => $object) {
97 5
                    $object['objectID'] = ObjectIdEncrypter::encrypt($searchable, (int) $part);
98 5
                    $objectsToSave[] = $object;
99
                }
100 5
                $searchablesToDelete[] = $searchable;
101
            } else {
102 8
                $array['objectID'] = ObjectIdEncrypter::encrypt($searchable);
103 13
                $objectsToSave[] = $array;
104
            }
105
        }
106
107 13
        dispatch_now(new DeleteJob(collect($searchablesToDelete)));
108
109 13
        $result = $index->saveObjects($objectsToSave);
110 13
        if (config('scout.synchronous', false)) {
111
            $result->wait();
112
        }
113 13
    }
114
115
    /**
116
     * @param  object $searchable
117
     *
118
     * @return bool
119
     */
120 13
    private function shouldBeSplitted($searchable): bool
121
    {
122 13
        $class = get_class($searchable->getModel());
123
124 13
        if (! array_key_exists($class, $this->splittables)) {
125 13
            $this->splittables[$class] = false;
126
127 13
            foreach ($searchable->toSearchableArray() as $key => $value) {
128 13
                $method = 'split'.Str::camel($key);
129 13
                $model = $searchable->getModel();
130 13
                if (method_exists($model, $method)) {
131 5
                    $this->splittables[$class] = true;
132 13
                    break;
133
                }
134
            }
135
        }
136
137 13
        return $this->splittables[$class];
138
    }
139
140
    /**
141
     * @param  object $searchable
142
     * @param  array $array
143
     *
144
     * @return array
145
     */
146 5
    private function splitSearchable($searchable, array $array): array
147
    {
148 5
        $pieces = [];
149 5
        foreach ($array as $key => $value) {
150 5
            $method = 'split'.Str::camel((string) $key);
151 5
            $model = $searchable->getModel();
152 5
            if (method_exists($model, $method)) {
153 5
                $result = $model->{$method}($value);
154 5
                $splittedBy = $key;
155 5
                $pieces[$splittedBy] = [];
156
                switch (true) {
157 5
                    case is_array($result):
158 3
                        $pieces[$splittedBy] = $result;
159 3
                        break;
160 3
                    case is_string($result):
161 1
                        $pieces[$splittedBy] = app($result)($model, $value);
162 1
                        break;
163 2
                    case is_object($result):
164 2
                        $pieces[$splittedBy] = $result->__invoke($model, $value);
165 5
                        break;
166
                }
167
            }
168
        }
169
170 5
        $objects = [[]];
171 5
        foreach ($pieces as $splittedBy => $values) {
172 5
            $temp = [];
173 5
            foreach ($objects as $object) {
174 5
                foreach ($values as $value) {
175 5
                    $temp[] = array_merge($object, [$splittedBy => $value]);
176
                }
177
            }
178 5
            $objects = $temp;
179
        }
180
181
        return array_map(function ($object) use ($array) {
182 5
            return array_merge($array, $object);
183 5
        }, $objects);
184
    }
185
186
    /**
187
     * Determine if the given searchable uses soft deletes.
188
     *
189
     * @param  object $searchable
190
     *
191
     * @return bool
192
     */
193 1
    private function usesSoftDelete($searchable): bool
194
    {
195 1
        return $searchable instanceof Model && in_array(SoftDeletes::class, class_uses_recursive($searchable), true);
196
    }
197
198
    /**
199
     * Mutate the given array using searchable's model attributes.
200
     *
201
     * @param  object  $searchable
202
     * @param  array  $array
203
     *
204
     * @return array
205
     */
206 13
    private function mutateArray($searchable, array $array): array
207
    {
208 13
        foreach ($array as $key => $value) {
209 13
            $attributeValue = $searchable->getModel()->getAttribute($key);
210
211
            /*
212
             * Casts carbon instances to timestamp.
213
             */
214 13
            if ($attributeValue instanceof \Illuminate\Support\Carbon) {
215 13
                $array[$key] = $attributeValue->getTimestamp();
216
            }
217
218
            /*
219
             * Casts numeric strings to integers/floats.
220
             */
221 13
            if (is_string($attributeValue) && is_numeric($attributeValue)) {
222
                $array[$key] = ctype_digit($attributeValue)
223
                    ? (int) $attributeValue
224 13
                    : (float) $attributeValue;
225
            }
226
        }
227
228 13
        return $array;
229
    }
230
}
231