Completed
Push — v0.10 ( f42b97...309689 )
by Simonas
8s
created

Repository::parseResult()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 43
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 43
rs 6.7272
cc 7
eloc 32
nc 7
nop 3
1
<?php
2
3
/*
4
 * This file is part of the ONGR package.
5
 *
6
 * (c) NFQ Technologies UAB <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ONGR\ElasticsearchBundle\ORM;
13
14
use Elasticsearch\Common\Exceptions\Missing404Exception;
15
use ONGR\ElasticsearchBundle\Document\DocumentInterface;
16
use ONGR\ElasticsearchBundle\DSL\Query\TermsQuery;
17
use ONGR\ElasticsearchBundle\DSL\Search;
18
use ONGR\ElasticsearchBundle\DSL\Sort\Sort;
19
use ONGR\ElasticsearchBundle\DSL\Suggester\AbstractSuggester;
20
use ONGR\ElasticsearchBundle\Result\Converter;
21
use ONGR\ElasticsearchBundle\Result\DocumentIterator;
22
use ONGR\ElasticsearchBundle\Result\DocumentScanIterator;
23
use ONGR\ElasticsearchBundle\Result\IndicesResult;
24
use ONGR\ElasticsearchBundle\Result\RawResultIterator;
25
use ONGR\ElasticsearchBundle\Result\RawResultScanIterator;
26
use ONGR\ElasticsearchBundle\Result\Suggestion\SuggestionIterator;
27
28
/**
29
 * Repository class.
30
 */
31
class Repository
32
{
33
    const RESULTS_ARRAY = 'array';
34
    const RESULTS_OBJECT = 'object';
35
    const RESULTS_RAW = 'raw';
36
    const RESULTS_RAW_ITERATOR = 'raw_iterator';
37
38
    /**
39
     * @var Manager
40
     */
41
    private $manager;
42
43
    /**
44
     * @var array
45
     */
46
    private $namespaces = [];
47
48
    /**
49
     * @var array
50
     */
51
    private $types = [];
52
53
    /**
54
     * @var array
55
     */
56
    private $fieldsCache = [];
57
58
    /**
59
     * Constructor.
60
     *
61
     * @param Manager $manager
62
     * @param array   $repositories
63
     */
64
    public function __construct($manager, $repositories)
65
    {
66
        $this->manager = $manager;
67
        $this->namespaces = $repositories;
68
        $this->types = $this->getTypes();
69
    }
70
71
    /**
72
     * @return array
73
     */
74
    public function getTypes()
75
    {
76
        $types = [];
77
        $meta = $this->getManager()->getBundlesMapping($this->namespaces);
78
79
        foreach ($meta as $namespace => $metadata) {
80
            $types[] = $metadata->getType();
81
        }
82
83
        return $types;
84
    }
85
86
    /**
87
     * Returns a single document data by ID or null if document is not found.
88
     *
89
     * @param string $id         Document Id to find.
90
     * @param string $resultType Result type returned.
91
     *
92
     * @return DocumentInterface|null
93
     *
94
     * @throws \LogicException
95
     */
96
    public function find($id, $resultType = self::RESULTS_OBJECT)
97
    {
98
        if (count($this->types) !== 1) {
99
            throw new \LogicException('Only one type must be specified for the find() method');
100
        }
101
102
        $params = [
103
            'index' => $this->getManager()->getConnection()->getIndexName(),
104
            'type' => $this->types[0],
105
            'id' => $id,
106
        ];
107
108
        try {
109
            $result = $this->getManager()->getConnection()->getClient()->get($params);
110
        } catch (Missing404Exception $e) {
111
            return null;
112
        }
113
114
        if ($resultType === self::RESULTS_OBJECT) {
115
            return (new Converter(
116
                $this->getManager()->getTypesMapping(),
117
                $this->getManager()->getBundlesMapping()
118
            ))->convertToDocument($result);
119
        }
120
121
        return $this->parseResult($result, $resultType, '');
122
    }
123
124
    /**
125
     * Finds entities by a set of criteria.
126
     *
127
     * @param array      $criteria   Example: ['group' => ['best', 'worst'], 'job' => 'medic'].
128
     * @param array|null $orderBy    Example: ['name' => 'ASC', 'surname' => 'DESC'].
129
     * @param int|null   $limit      Example: 5.
130
     * @param int|null   $offset     Example: 30.
131
     * @param string     $resultType Result type returned.
132
     *
133
     * @return array|DocumentIterator The objects.
134
     */
135
    public function findBy(
136
        array $criteria,
137
        array $orderBy = [],
138
        $limit = null,
139
        $offset = null,
140
        $resultType = self::RESULTS_OBJECT
141
    ) {
142
        $search = $this->createSearch();
143
144
        if ($limit !== null) {
145
            $search->setSize($limit);
146
        }
147
        if ($offset !== null) {
148
            $search->setFrom($offset);
149
        }
150
151 View Code Duplication
        foreach ($criteria as $field => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
152
            $search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must');
153
        }
154
155 View Code Duplication
        foreach ($orderBy as $field => $direction) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
            $search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC));
157
        }
158
159
        return $this->execute($search, $resultType);
160
    }
161
162
    /**
163
     * Finds only one entity by a set of criteria.
164
     *
165
     * @param array      $criteria   Example: ['group' => ['best', 'worst'], 'job' => 'medic'].
166
     * @param array|null $orderBy    Example: ['name' => 'ASC', 'surname' => 'DESC'].
167
     * @param string     $resultType Result type returned.
168
     *
169
     * @return DocumentInterface|null The object.
170
     */
171
    public function findOneBy(array $criteria, array $orderBy = [], $resultType = self::RESULTS_OBJECT)
172
    {
173
        $search = $this->createSearch();
174
        $search->setSize(1);
175
176 View Code Duplication
        foreach ($criteria as $field => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
            $search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must');
178
        }
179
180 View Code Duplication
        foreach ($orderBy as $field => $direction) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
181
            $search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC));
182
        }
183
184
        $result = $this
185
            ->getManager()
186
            ->getConnection()
187
            ->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams());
188
189
        if ($resultType === self::RESULTS_OBJECT) {
190
            $rawData = $result['hits']['hits'];
191
            if (!count($rawData)) {
192
                return null;
193
            }
194
195
            return (new Converter(
196
                $this->getManager()->getTypesMapping(),
197
                $this->getManager()->getBundlesMapping()
198
            ))->convertToDocument($rawData[0]);
199
        }
200
201
        return $this->parseResult($result, $resultType, '');
202
    }
203
204
    /**
205
     * Returns search instance.
206
     *
207
     * @return Search
208
     */
209
    public function createSearch()
210
    {
211
        return new Search();
212
    }
213
214
    /**
215
     * Executes given search.
216
     *
217
     * @param Search $search
218
     * @param string $resultsType
219
     *
220
     * @return DocumentIterator|DocumentScanIterator|RawResultIterator|array
221
     *
222
     * @throws \Exception
223
     */
224
    public function execute(Search $search, $resultsType = self::RESULTS_OBJECT)
225
    {
226
        $results = $this
227
            ->getManager()
228
            ->getConnection()
229
            ->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams());
230
231
        return $this->parseResult($results, $resultsType, $search->getScroll());
232
    }
233
234
    /**
235
     * Delete by query.
236
     *
237
     * @param Search $search
238
     *
239
     * @return array
240
     */
241
    public function deleteByQuery(Search $search)
242
    {
243
        $results = $this
244
            ->getManager()
245
            ->getConnection()
246
            ->deleteByQuery($this->types, $search->toArray());
247
248
        return new IndicesResult($results);
249
    }
250
251
    /**
252
     * Fetches next set of results.
253
     *
254
     * @param string $scrollId
255
     * @param string $scrollDuration
256
     * @param string $resultsType
257
     *
258
     * @return array|DocumentScanIterator
259
     *
260
     * @throws \Exception
261
     */
262
    public function scan(
263
        $scrollId,
264
        $scrollDuration = '5m',
265
        $resultsType = self::RESULTS_OBJECT
266
    ) {
267
        $results = $this->getManager()->getConnection()->scroll($scrollId, $scrollDuration);
268
269
        return $this->parseResult($results, $resultsType, $scrollDuration);
270
    }
271
272
    /**
273
     * Get suggestions using suggest api.
274
     *
275
     * @param AbstractSuggester[]|AbstractSuggester $suggesters
276
     *
277
     * @return SuggestionIterator
278
     */
279
    public function suggest($suggesters)
280
    {
281
        if (!is_array($suggesters)) {
282
            $suggesters = [$suggesters];
283
        }
284
285
        $body = [];
286
        /** @var AbstractSuggester $suggester */
287
        foreach ($suggesters as $suggester) {
288
            $body = array_merge($suggester->toArray(), $body);
289
        }
290
        $results = $this->getManager()->getConnection()->getClient()->suggest(['body' => $body]);
291
        unset($results['_shards']);
292
293
        return new SuggestionIterator($results);
294
    }
295
296
    /**
297
     * Removes a single document data by ID.
298
     *
299
     * @param string $id Document ID to remove.
300
     *
301
     * @return array
302
     *
303
     * @throws \LogicException
304
     */
305
    public function remove($id)
306
    {
307
        if (count($this->types) == 1) {
308
            $params = [
309
                'index' => $this->getManager()->getConnection()->getIndexName(),
310
                'type' => $this->types[0],
311
                'id' => $id,
312
            ];
313
314
            $response = $this->getManager()->getConnection()->delete($params);
315
316
            return $response;
317
        } else {
318
            throw new \LogicException('Only one type must be specified for the find() method');
319
        }
320
    }
321
322
    /**
323
     * Checks if all required fields are added.
324
     *
325
     * @param array $searchArray
326
     * @param array $fields
327
     *
328
     * @return array
329
     */
330
    private function checkFields($searchArray, $fields = ['_parent', '_ttl'])
331
    {
332
        if (empty($fields)) {
333
            return $searchArray;
334
        }
335
336
        // Checks if cache is loaded.
337
        if (empty($this->fieldsCache)) {
338
            foreach ($this->getManager()->getBundlesMapping($this->namespaces) as $ns => $properties) {
339
                $this->fieldsCache = array_unique(
340
                    array_merge(
341
                        $this->fieldsCache,
342
                        array_keys($properties->getFields())
343
                    )
344
                );
345
            }
346
        }
347
348
        // Adds cached fields to fields array.
349
        foreach (array_intersect($this->fieldsCache, $fields) as $field) {
350
            $searchArray['fields'][] = $field;
351
        }
352
353
        // Removes duplicates and checks if its needed to add _source.
354
        if (!empty($searchArray['fields'])) {
355
            $searchArray['fields'] = array_unique($searchArray['fields']);
356
            if (array_diff($searchArray['fields'], $fields) === []) {
357
                $searchArray['fields'][] = '_source';
358
            }
359
        }
360
361
        return $searchArray;
362
    }
363
364
    /**
365
     * Parses raw result.
366
     *
367
     * @param array  $raw
368
     * @param string $resultsType
369
     * @param string $scrollDuration
370
     *
371
     * @return DocumentIterator|DocumentScanIterator|RawResultIterator|array
372
     *
373
     * @throws \Exception
374
     */
375
    private function parseResult($raw, $resultsType, $scrollDuration)
376
    {
377
        switch ($resultsType) {
378
            case self::RESULTS_OBJECT:
379
                if (isset($raw['_scroll_id'])) {
380
                    $iterator = new DocumentScanIterator(
381
                        $raw,
382
                        $this->getManager()->getTypesMapping(),
383
                        $this->getManager()->getBundlesMapping()
384
                    );
385
                    $iterator
386
                        ->setRepository($this)
387
                        ->setScrollDuration($scrollDuration)
388
                        ->setScrollId($raw['_scroll_id']);
389
390
                    return $iterator;
391
                }
392
393
                return new DocumentIterator(
394
                    $raw,
395
                    $this->getManager()->getTypesMapping(),
396
                    $this->getManager()->getBundlesMapping()
397
                );
398
            case self::RESULTS_ARRAY:
399
                return $this->convertToNormalizedArray($raw);
400
            case self::RESULTS_RAW:
401
                return $raw;
402
            case self::RESULTS_RAW_ITERATOR:
403
                if (isset($raw['_scroll_id'])) {
404
                    $iterator = new RawResultScanIterator($raw);
405
                    $iterator
406
                        ->setRepository($this)
407
                        ->setScrollDuration($scrollDuration)
408
                        ->setScrollId($raw['_scroll_id']);
409
410
                    return $iterator;
411
                }
412
413
                return new RawResultIterator($raw);
414
            default:
415
                throw new \Exception('Wrong results type selected');
416
        }
417
    }
418
419
    /**
420
     * Normalizes response array.
421
     *
422
     * @param array $data
423
     *
424
     * @return array
425
     */
426
    private function convertToNormalizedArray($data)
427
    {
428
        if (array_key_exists('_source', $data)) {
429
            return $data['_source'];
430
        }
431
432
        $output = [];
433
434
        if (isset($data['hits']['hits'][0]['_source'])) {
435
            foreach ($data['hits']['hits'] as $item) {
436
                $output[] = $item['_source'];
437
            }
438
        } elseif (isset($data['hits']['hits'][0]['fields'])) {
439
            foreach ($data['hits']['hits'] as $item) {
440
                $output[] = array_map('reset', $item['fields']);
441
            }
442
        }
443
444
        return $output;
445
    }
446
447
    /**
448
     * Creates new instance of document.
449
     *
450
     * @return DocumentInterface
451
     *
452
     * @throws \LogicException
453
     */
454
    public function createDocument()
455
    {
456
        if (count($this->namespaces) > 1) {
457
            throw new \LogicException(
458
                'Repository can not create new document when it is associated with multiple namespaces'
459
            );
460
        }
461
462
        $class = $this->getManager()->getBundlesMapping()[reset($this->namespaces)]->getProxyNamespace();
463
464
        return new $class();
465
    }
466
467
    /**
468
     * Returns elasticsearch manager used in this repository for getting/setting documents.
469
     *
470
     * @return Manager
471
     */
472
    public function getManager()
473
    {
474
        return $this->manager;
475
    }
476
}
477