MediaExtractionService::elasticsearch()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace App\Services\AdditionalProcessing;
4
5
use App\Facades\Search;
6
use App\Models\Category;
7
use App\Models\Release;
8
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
0 ignored issues
show
Bug introduced by
The type App\Services\AdditionalP...ProcessingConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext;
10
use App\Services\Categorization\CategorizationService;
11
use App\Services\ReleaseExtraService;
12
use App\Services\ReleaseImageService;
13
use FFMpeg\Coordinate\Dimension;
14
use FFMpeg\Coordinate\TimeCode;
15
use FFMpeg\FFMpeg;
16
use FFMpeg\FFProbe;
17
use FFMpeg\Filters\Video\ResizeFilter;
18
use FFMpeg\Format\Audio\Vorbis;
19
use FFMpeg\Format\Video\Ogg;
20
use Illuminate\Support\Facades\File;
21
use Illuminate\Support\Facades\Log;
22
use Mhor\MediaInfo\MediaInfo;
23
24
/**
25
 * Service for processing media files (video, audio, images).
26
 * Handles sample generation, thumbnails, media info extraction, and audio processing.
27
 */
28
class MediaExtractionService
29
{
30
    private ?FFMpeg $ffmpeg = null;
31
32
    private ?FFProbe $ffprobe = null;
33
34
    private ?MediaInfo $mediaInfo = null;
35
36
    public function __construct(
37
        private readonly ProcessingConfiguration $config,
38
        private readonly ReleaseImageService $releaseImage,
39
        private readonly ReleaseExtraService $releaseExtra,
40
        private readonly CategorizationService $categorize
41
    ) {}
42
43
    /**
44
     * Get video time code for sample extraction.
45
     */
46
    public function getVideoTime(string $videoLocation): string
47
    {
48
        try {
49
            if (! $this->ffprobe()->isValid($videoLocation)) {
50
                return '';
51
            }
52
            $time = $this->ffprobe()->format($videoLocation)->get('duration');
53
        } catch (\Throwable $e) {
54
            if ($this->config->debugMode) {
55
                Log::debug($e->getMessage());
56
            }
57
58
            return '';
59
        }
60
61
        if (empty($time) || ! preg_match('/time=(\d{1,2}:\d{1,2}:)?(\d{1,2})\.(\d{1,2})\s*bitrate=/i', $time, $numbers)) {
62
            return '';
63
        }
64
65
        if ($numbers[3] > 0) {
66
            $numbers[3]--;
67
        } elseif ($numbers[1] > 0) {
68
            $numbers[2]--;
69
            $numbers[3] = '99';
70
        }
71
72
        return '00:00:'.str_pad($numbers[2], 2, '0', STR_PAD_LEFT).'.'.str_pad($numbers[3], 2, '0', STR_PAD_LEFT);
73
    }
74
75
    /**
76
     * Extract a sample image from a video file.
77
     */
78
    public function getSample(string $fileLocation, string $tmpPath, string $guid): bool
79
    {
80
        if (! $this->config->processThumbnails || ! File::isFile($fileLocation)) {
81
            return false;
82
        }
83
84
        $fileName = $tmpPath.'zzzz'.random_int(5, 12).random_int(5, 12).'.jpg';
85
        $time = $this->getVideoTime($fileLocation);
86
87
        try {
88
            if ($this->ffprobe()->isValid($fileLocation)) {
89
                $this->ffmpeg()->open($fileLocation)
90
                    ->frame(TimeCode::fromString($time === '' ? '00:00:03:00' : $time))
0 ignored issues
show
Bug introduced by
The method frame() does not exist on FFMpeg\Media\Audio. It seems like you code against a sub-type of FFMpeg\Media\Audio such as FFMpeg\Media\Video. ( Ignorable by Annotation )

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

90
                    ->/** @scrutinizer ignore-call */ frame(TimeCode::fromString($time === '' ? '00:00:03:00' : $time))
Loading history...
91
                    ->save($fileName);
92
            }
93
        } catch (\Throwable $e) {
94
            if ($this->config->debugMode) {
95
                Log::error($e->getTraceAsString());
96
            }
97
98
            return false;
99
        }
100
101
        if (! File::isFile($fileName)) {
102
            return false;
103
        }
104
105
        $saved = $this->releaseImage->saveImage(
106
            $guid.'_thumb',
107
            $fileName,
108
            $this->releaseImage->imgSavePath,
109
            800,
110
            600
111
        );
112
113
        File::delete($fileName);
114
115
        return $saved === 1;
116
    }
117
118
    /**
119
     * Create a video sample clip.
120
     */
121
    public function getVideo(string $fileLocation, string $tmpPath, string $guid): bool
122
    {
123
        if (! $this->config->processVideo || ! File::isFile($fileLocation)) {
124
            return false;
125
        }
126
127
        $fileName = $tmpPath.'zzzz'.$guid.'.ogv';
128
        $newMethod = false;
129
130
        // Try to get sample from end of video if duration is short
131
        if ($this->config->ffmpegDuration < 60) {
132
            $time = $this->getVideoTime($fileLocation);
133
            if ($time !== '' && preg_match('/(\d{2}).(\d{2})/', $time, $numbers)) {
134
                $newMethod = true;
135
                if ($numbers[1] <= $this->config->ffmpegDuration) {
136
                    $lowestLength = '00:00:00.00';
137
                } else {
138
                    $lowestLength = ($numbers[1] - $this->config->ffmpegDuration);
139
                    $end = '.'.$numbers[2];
140
                    $lowestLength = match (strlen($lowestLength)) {
141
                        1 => '00:00:0'.$lowestLength.$end,
142
                        2 => '00:00:'.$lowestLength.$end,
143
                        default => '00:00:60.00',
144
                    };
145
                }
146
147
                try {
148
                    if ($this->ffprobe()->isValid($fileLocation)) {
149
                        $video = $this->ffmpeg()->open($fileLocation);
150
                        $clip = $video->clip(
0 ignored issues
show
Bug introduced by
The method clip() does not exist on FFMpeg\Media\Audio. It seems like you code against a sub-type of FFMpeg\Media\Audio such as FFMpeg\Media\Video. ( Ignorable by Annotation )

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

150
                        /** @scrutinizer ignore-call */ 
151
                        $clip = $video->clip(
Loading history...
151
                            TimeCode::fromString($lowestLength),
152
                            TimeCode::fromSeconds($this->config->ffmpegDuration)
153
                        );
154
                        $format = new Ogg;
155
                        $format->setAudioCodec('libvorbis');
156
                        $clip->filters()->resize(new Dimension(320, -1), ResizeFilter::RESIZEMODE_SCALE_HEIGHT);
157
                        $clip->save($format, $fileName);
158
                    }
159
                } catch (\Throwable $e) {
160
                    if ($this->config->debugMode) {
161
                        Log::error($e->getTraceAsString());
162
                    }
163
                }
164
            }
165
        }
166
167
        // Fallback: use start of video
168
        if (! $newMethod) {
169
            try {
170
                if ($this->ffprobe()->isValid($fileLocation)) {
171
                    $video = $this->ffmpeg()->open($fileLocation);
172
                    $clip = $video->clip(
173
                        TimeCode::fromSeconds(0),
174
                        TimeCode::fromSeconds($this->config->ffmpegDuration)
175
                    );
176
                    $format = new Ogg;
177
                    $format->setAudioCodec('libvorbis');
178
                    $clip->filters()->resize(new Dimension(320, -1), ResizeFilter::RESIZEMODE_SCALE_HEIGHT);
179
                    $clip->save($format, $fileName);
180
                }
181
            } catch (\Throwable $e) {
182
                if ($this->config->debugMode) {
183
                    Log::error($e->getTraceAsString());
184
                }
185
            }
186
        }
187
188
        if (! File::isFile($fileName)) {
189
            return false;
190
        }
191
192
        $newFile = $this->releaseImage->vidSavePath.$guid.'.ogv';
193
194
        if (! @File::move($fileName, $newFile)) {
195
            $copied = @File::copy($fileName, $newFile);
196
            File::delete($fileName);
197
            if (! $copied) {
198
                return false;
199
            }
200
        }
201
202
        @chmod($newFile, 0764);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

202
        /** @scrutinizer ignore-unhandled */ @chmod($newFile, 0764);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
203
        Release::query()->where('guid', $guid)->update(['videostatus' => 1]);
204
205
        return true;
206
    }
207
208
    /**
209
     * Extract media info from a video file.
210
     */
211
    public function getMediaInfo(string $fileLocation, int $releaseId): bool
212
    {
213
        if (! $this->config->processMediaInfo || ! File::isFile($fileLocation)) {
214
            return false;
215
        }
216
217
        try {
218
            $xmlArray = $this->mediaInfo()->getInfo($fileLocation, true);
219
            \App\Models\MediaInfo::addData($releaseId, $xmlArray);
220
            $this->releaseExtra->addFromXml($releaseId, $xmlArray);
221
222
            return true;
223
        } catch (\Throwable $e) {
224
            Log::debug($e->getMessage());
225
226
            return false;
227
        }
228
    }
229
230
    /**
231
     * Process a JPG sample image.
232
     */
233
    public function getJPGSample(string $fileLocation, string $guid): bool
234
    {
235
        $saved = $this->releaseImage->saveImage(
236
            $guid.'_thumb',
237
            $fileLocation,
238
            $this->releaseImage->jpgSavePath,
239
            650,
240
            650
241
        );
242
243
        if ($saved === 1) {
244
            Release::query()->where('guid', $guid)->update(['jpgstatus' => 1]);
245
246
            return true;
247
        }
248
249
        return false;
250
    }
251
252
    /**
253
     * Process audio file for media info and sample.
254
     *
255
     * @return array{audioInfo: bool, audioSample: bool}
256
     */
257
    public function getAudioInfo(
258
        string $fileLocation,
259
        string $fileExtension,
260
        ReleaseProcessingContext $context,
261
        string $tmpPath
262
    ): array {
263
        $result = ['audioInfo' => false, 'audioSample' => false];
264
265
        if (! $this->config->processAudioSample) {
266
            $result['audioSample'] = true;
267
        }
268
        if (! $this->config->processAudioInfo) {
269
            $result['audioInfo'] = true;
270
        }
271
272
        $rQuery = Release::query()
273
            ->where('proc_pp', '=', 0)
274
            ->where('id', $context->release->id)
275
            ->select(['searchname', 'fromname', 'categories_id', 'groups_id'])
276
            ->first();
277
278
        $musicParent = (string) Category::MUSIC_ROOT;
279
        if ($rQuery === null || ! preg_match(
280
            sprintf(
281
                '/%d\d{3}|%d|%d|%d/',
282
                $musicParent[0],
283
                Category::OTHER_MISC,
284
                Category::MOVIE_OTHER,
285
                Category::TV_OTHER
286
            ),
287
            $rQuery->categories_id
0 ignored issues
show
Bug introduced by
The property categories_id does not exist on App\Models\Release. Did you mean category_ids?
Loading history...
288
        )) {
289
            return $result;
290
        }
291
292
        if (! File::isFile($fileLocation)) {
293
            return $result;
294
        }
295
296
        // Get media info
297
        if (! $result['audioInfo']) {
298
            try {
299
                $xmlArray = $this->mediaInfo()->getInfo($fileLocation, false);
300
                if ($xmlArray !== null) {
301
                    foreach ($xmlArray->getAudios() as $track) {
302
                        if ($track->get('album') !== null && $track->get('performer') !== null) {
303
                            if ((int) $context->release->predb_id === 0 && $this->config->renameMusicMediaInfo) {
0 ignored issues
show
Bug introduced by
The property predb_id does not exist on App\Models\Release. Did you mean predb?
Loading history...
304
                                $ext = strtoupper($fileExtension);
305
306
                                $newName = $track->get('performer')->getFullName().' - '.$track->get('album')->getFullName();
307
                                if (! empty($track->get('recorded_date'))
308
                                    && preg_match('/(?:19|20)\d\d/', $track->get('recorded_date')->getFullname, $Year)
309
                                ) {
310
                                    $newName .= ' ('.$Year[0].') '.$ext;
311
                                } else {
312
                                    $newName .= ' '.$ext;
313
                                }
314
315
                                $newCat = match ($ext) {
316
                                    'MP3' => Category::MUSIC_MP3,
317
                                    'FLAC' => Category::MUSIC_LOSSLESS,
318
                                    default => $this->categorize->determineCategory($rQuery->groups_id, $newName, $rQuery->fromname),
0 ignored issues
show
Bug introduced by
The property groups_id does not exist on App\Models\Release. Did you mean group?
Loading history...
Bug introduced by
The property fromname does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
319
                                };
320
321
                                $newTitle = escapeString(substr($newName, 0, 255));
322
                                Release::whereId($context->release->id)->update([
323
                                    'searchname' => $newTitle,
324
                                    'categories_id' => is_array($newCat) ? $newCat['categories_id'] : $newCat,
325
                                    'iscategorized' => 1,
326
                                    'isrenamed' => 1,
327
                                    'proc_pp' => 1,
328
                                ]);
329
330
                                Search::updateRelease($context->release->id);
331
332
                                if ($this->config->echoCLI) {
333
                                    NameFixer::echoChangedReleaseName([
0 ignored issues
show
Bug introduced by
The type App\Services\AdditionalProcessing\NameFixer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
334
                                        'new_name' => $newTitle,
335
                                        'old_name' => $rQuery->searchname,
0 ignored issues
show
Bug introduced by
The property searchname does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
336
                                        'new_category' => $newCat,
337
                                        'old_category' => $rQuery->categories_id,
338
                                        'group' => $rQuery->groups_id,
339
                                        'releases_id' => $context->release->id,
340
                                        'method' => 'MediaExtractionService->getAudioInfo',
341
                                    ]);
342
                                }
343
                            }
344
345
                            $this->releaseExtra->addFromXml($context->release->id, $xmlArray);
346
                            $result['audioInfo'] = true;
347
                            $context->foundAudioInfo = true;
348
                            break;
349
                        }
350
                    }
351
                }
352
            } catch (\Throwable $e) {
353
                Log::debug($e->getMessage());
354
            }
355
        }
356
357
        // Create audio sample
358
        if (! $result['audioSample']) {
359
            $audioFileName = $context->release->guid.'.ogg';
0 ignored issues
show
Bug introduced by
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
360
361
            try {
362
                if ($this->ffprobe()->isValid($fileLocation)) {
363
                    $audioSample = $this->ffmpeg()->open($fileLocation);
364
                    $format = new Vorbis;
365
                    $audioSample->clip(TimeCode::fromSeconds(30), TimeCode::fromSeconds(30));
366
                    $audioSample->save($format, $tmpPath.$audioFileName);
367
                }
368
            } catch (\Throwable $e) {
369
                if ($this->config->debugMode) {
370
                    Log::error($e->getTraceAsString());
371
                }
372
            }
373
374
            if (File::isFile($tmpPath.$audioFileName)) {
375
                $renamed = File::move($tmpPath.$audioFileName, $this->config->audioSavePath.$audioFileName);
376
                if (! $renamed) {
377
                    $copied = File::copy($tmpPath.$audioFileName, $this->config->audioSavePath.$audioFileName);
378
                    File::delete($tmpPath.$audioFileName);
379
                    if (! $copied) {
380
                        return $result;
381
                    }
382
                }
383
384
                @chmod($this->config->audioSavePath.$audioFileName, 0764);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

384
                /** @scrutinizer ignore-unhandled */ @chmod($this->config->audioSavePath.$audioFileName, 0764);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
385
                Release::query()->where('id', $context->release->id)->update(['audiostatus' => 1]);
386
                $result['audioSample'] = true;
387
                $context->foundAudioSample = true;
388
            }
389
        }
390
391
        return $result;
392
    }
393
394
    /**
395
     * Process a video file for sample, video clip, and media info.
396
     */
397
    public function processVideoFile(
398
        string $fileLocation,
399
        ReleaseProcessingContext $context,
400
        string $tmpPath
401
    ): array {
402
        $result = [
403
            'sample' => false,
404
            'video' => false,
405
            'mediaInfo' => false,
406
        ];
407
408
        if (! $context->foundSample) {
409
            $result['sample'] = $this->getSample($fileLocation, $tmpPath, $context->release->guid);
0 ignored issues
show
Bug introduced by
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
410
            if ($result['sample']) {
411
                $context->foundSample = true;
412
            }
413
        }
414
415
        // Only get video if sampleMessageIDs count is less than 2
416
        if (! $context->foundVideo && count($context->sampleMessageIDs) < 2) {
417
            $result['video'] = $this->getVideo($fileLocation, $tmpPath, $context->release->guid);
418
            if ($result['video']) {
419
                $context->foundVideo = true;
420
            }
421
        }
422
423
        if (! $context->foundMediaInfo) {
424
            $result['mediaInfo'] = $this->getMediaInfo($fileLocation, $context->release->id);
425
            if ($result['mediaInfo']) {
426
                $context->foundMediaInfo = true;
427
            }
428
        }
429
430
        return $result;
431
    }
432
433
    /**
434
     * Check if data appears to be a JPEG image.
435
     */
436
    public function isJpegData(string $filePath): bool
437
    {
438
        if (! File::isFile($filePath)) {
439
            return false;
440
        }
441
442
        return exif_imagetype($filePath) === IMAGETYPE_JPEG;
443
    }
444
445
    /**
446
     * Check if file is a valid image (JPEG or PNG).
447
     */
448
    public function isValidImage(string $filePath): bool
449
    {
450
        if (! File::isFile($filePath)) {
451
            return false;
452
        }
453
454
        $type = @exif_imagetype($filePath);
455
456
        return $type === IMAGETYPE_JPEG || $type === IMAGETYPE_PNG;
457
    }
458
459
    private function ffmpeg(): FFMpeg
460
    {
461
        if ($this->ffmpeg === null) {
462
            $timeout = $this->config->timeoutSeconds > 0 ? $this->config->timeoutSeconds : 60;
463
            $this->ffmpeg = FFMpeg::create(['timeout' => $timeout]);
464
        }
465
466
        return $this->ffmpeg;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->ffmpeg could return the type null which is incompatible with the type-hinted return FFMpeg\FFMpeg. Consider adding an additional type-check to rule them out.
Loading history...
467
    }
468
469
    private function ffprobe(): FFProbe
470
    {
471
        if ($this->ffprobe === null) {
472
            $this->ffprobe = FFProbe::create();
473
        }
474
475
        return $this->ffprobe;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->ffprobe could return the type null which is incompatible with the type-hinted return FFMpeg\FFProbe. Consider adding an additional type-check to rule them out.
Loading history...
476
    }
477
478
    private function mediaInfo(): MediaInfo
479
    {
480
        if ($this->mediaInfo === null) {
481
            $this->mediaInfo = new MediaInfo;
482
            $this->mediaInfo->setConfig('use_oldxml_mediainfo_output_format', true);
0 ignored issues
show
Bug introduced by
The method setConfig() does not exist on null. ( Ignorable by Annotation )

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

482
            $this->mediaInfo->/** @scrutinizer ignore-call */ 
483
                              setConfig('use_oldxml_mediainfo_output_format', true);

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...
483
            if ($this->config->mediaInfoPath) {
484
                $this->mediaInfo->setConfig('command', $this->config->mediaInfoPath);
485
            }
486
        }
487
488
        return $this->mediaInfo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mediaInfo could return the type null which is incompatible with the type-hinted return Mhor\MediaInfo\MediaInfo. Consider adding an additional type-check to rule them out.
Loading history...
489
    }
490
491
    private function manticore(): ManticoreSearchService
0 ignored issues
show
Bug introduced by
The type App\Services\AdditionalP...\ManticoreSearchService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Unused Code introduced by
The method manticore() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
492
    {
493
        if ($this->manticore === null) {
494
            $this->manticore = app(ManticoreSearchService::class);
0 ignored issues
show
Bug Best Practice introduced by
The property manticore does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
495
        }
496
497
        return $this->manticore;
498
    }
499
500
    private function elasticsearch(): ElasticSearchService
0 ignored issues
show
Bug introduced by
The type App\Services\AdditionalP...ng\ElasticSearchService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Unused Code introduced by
The method elasticsearch() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
501
    {
502
        if ($this->elasticsearch === null) {
503
            $this->elasticsearch = app(ElasticSearchService::class);
0 ignored issues
show
Bug Best Practice introduced by
The property elasticsearch does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
504
        }
505
506
        return $this->elasticsearch;
507
    }
508
}
509