Completed
Push — search_feature_flags ( 457ab1 )
by André
13:46
created

Handler::supports()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
rs 10
1
<?php
2
3
/**
4
 * File containing the Content Search handler class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Search\Elasticsearch\Content;
10
11
use eZ\Publish\API\Repository\Values\Content\Query;
12
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
13
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
14
use eZ\Publish\SPI\Persistence\Content;
15
use eZ\Publish\SPI\Search\Handler as SearchHandlerInterface;
16
use eZ\Publish\SPI\Persistence\Content\Location;
17
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
18
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
19
20
class Handler implements SearchHandlerInterface
21
{
22
    /**
23
     * @var \eZ\Publish\Core\Search\Elasticsearch\Content\Gateway
24
     */
25
    protected $gateway;
26
27
    /**
28
     * @var \eZ\Publish\Core\Search\Elasticsearch\Content\Gateway
29
     */
30
    protected $locationGateway;
31
32
    /**
33
     * @var \eZ\Publish\Core\Search\Elasticsearch\Content\MapperInterface
34
     */
35
    protected $mapper;
36
37
    /**
38
     * Search result extractor.
39
     *
40
     * @var \eZ\Publish\Core\Search\Elasticsearch\Content\Extractor
41
     */
42
    protected $extractor;
43
44
    /**
45
     * Identifier of Content document type in the search backend.
46
     *
47
     * @var string
48
     */
49
    protected $contentDocumentTypeIdentifier;
50
51
    /**
52
     * Identifier of Location document type in the search backend.
53
     *
54
     * @var string
55
     */
56
    protected $locationDocumentTypeIdentifier;
57
58
    public function __construct(
59
        Gateway $gateway,
60
        Gateway $locationGateway,
61
        MapperInterface $mapper,
62
        Extractor $extractor,
63
        $contentDocumentTypeIdentifier,
64
        $locationDocumentTypeIdentifier
65
    ) {
66
        $this->gateway = $gateway;
67
        $this->locationGateway = $locationGateway;
68
        $this->mapper = $mapper;
69
        $this->extractor = $extractor;
70
        $this->contentDocumentTypeIdentifier = $contentDocumentTypeIdentifier;
71
        $this->locationDocumentTypeIdentifier = $locationDocumentTypeIdentifier;
72
    }
73
74
    /**
75
     * Finds content objects for the given query.
76
     *
77
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if Query criterion is not applicable to its target
78
     *
79
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
80
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
81
     *        Also used to define which field languages are loaded for the returned content.
82
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
83
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations
84
     *
85
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
86
     */
87 View Code Duplication
    public function findContent(Query $query, array $languageFilter = array())
88
    {
89
        $query->filter = $query->filter ?: new Criterion\MatchAll();
90
        $query->query = $query->query ?: new Criterion\MatchAll();
91
92
        $data = $this->gateway->find($query, $this->contentDocumentTypeIdentifier, $languageFilter);
93
94
        return $this->extractor->extract($data);
95
    }
96
97
    /**
98
     * Performs a query for a single content object.
99
     *
100
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the object was not found by the query or due to permissions
101
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if Criterion is not applicable to its target
102
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is more than than one result matching the criterions
103
     *
104
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
105
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
106
     *        Also used to define which field languages are loaded for the returned content.
107
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
108
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations
109
     *
110
     * @return \eZ\Publish\SPI\Persistence\Content
111
     */
112
    public function findSingle(Criterion $filter, array $languageFilter = array())
113
    {
114
        $query = new Query();
115
        $query->filter = $filter;
116
        $query->offset = 0;
117
        $query->limit = 1;
118
        $result = $this->findContent($query, $languageFilter);
119
120
        if (!$result->totalCount) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result->totalCount of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
121
            throw new NotFoundException(
122
                'Content',
123
                'findSingle() found no content for given $filter'
124
            );
125
        } elseif ($result->totalCount > 1) {
126
            throw new InvalidArgumentException(
127
                'totalCount',
128
                'findSingle() found more then one item for given $filter'
129
            );
130
        }
131
132
        return $result->searchHits[0]->valueObject;
133
    }
134
135
    /**
136
     * Finds Locations for the given $query.
137
     *
138
     * @param \eZ\Publish\API\Repository\Values\Content\LocationQuery $query
139
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
140
     *        Also used to define which field languages are loaded for the returned content.
141
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
142
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations
143
     *
144
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
145
     */
146 View Code Duplication
    public function findLocations(LocationQuery $query, array $languageFilter = array())
147
    {
148
        $query->filter = $query->filter ?: new Criterion\MatchAll();
149
        $query->query = $query->query ?: new Criterion\MatchAll();
150
151
        $data = $this->locationGateway->find($query, $this->locationDocumentTypeIdentifier);
152
153
        return $this->extractor->extract($data);
154
    }
155
156
    /**
157
     * Suggests a list of values for the given prefix.
158
     *
159
     * @param string $prefix
160
     * @param string[] $fieldPaths
161
     * @param int $limit
162
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
163
     */
164
    public function suggest($prefix, $fieldPaths = array(), $limit = 10, Criterion $filter = null)
165
    {
166
        // TODO: Implement suggest() method.
167
    }
168
169
    /**
170
     * Indexes a content object.
171
     *
172
     * @param \eZ\Publish\SPI\Persistence\Content $content
173
     */
174
    public function indexContent(Content $content)
175
    {
176
        $document = $this->mapper->mapContent($content);
177
178
        $this->gateway->index($document);
179
    }
180
181
    /**
182
     * Indexes several content objects.
183
     *
184
     * @todo: This function and setCommit() is needed for Persistence\Solr for test speed but not part
185
     *       of interface for the reason described in Solr\Content\Search\Gateway\Native::bulkIndexContent
186
     *       Short: Bulk handling should be properly designed before added to the interface.
187
     *
188
     * @param \eZ\Publish\SPI\Persistence\Content[] $contentObjects
189
     */
190
    public function bulkIndexContent(array $contentObjects)
191
    {
192
        $documents = array();
193
        foreach ($contentObjects as $content) {
194
            $documents[] = $this->mapper->mapContent($content);
195
        }
196
197
        $this->gateway->bulkIndex($documents);
198
    }
199
200
    /**
201
     * Indexes a Location in the index storage.
202
     *
203
     * @param \eZ\Publish\SPI\Persistence\Content\Location $location
204
     */
205
    public function indexLocation(Location $location)
206
    {
207
        $document = $this->mapper->mapLocation($location);
208
209
        $this->gateway->index($document);
210
    }
211
212
    /**
213
     * Indexes several Locations.
214
     *
215
     * @todo: This function and setCommit() is needed for Persistence\Solr for test speed but not part
216
     *       of interface for the reason described in Solr\Content\Search\Gateway\Native::bulkIndexContent
217
     *       Short: Bulk handling should be properly designed before added to the interface.
218
     *
219
     * @param \eZ\Publish\SPI\Persistence\Content\Location[] $locations
220
     */
221
    public function bulkIndexLocations(array $locations)
222
    {
223
        $documents = array();
224
        foreach ($locations as $location) {
225
            $documents[] = $this->mapper->mapLocation($location);
226
        }
227
228
        $this->gateway->bulkIndex($documents);
229
    }
230
231
    /**
232
     * Deletes a content object from the index.
233
     *
234
     * @param int $contentId
235
     * @param int|null $versionId
236
     */
237
    public function deleteContent($contentId, $versionId = null)
238
    {
239
        // 1. Delete the Content
240
        if ($versionId === null) {
241
            $this->gateway->deleteByQuery(json_encode(['query' => ['match' => ['_id' => $contentId]]]), $this->contentDocumentTypeIdentifier);
242
        } else {
243
            $this->gateway->delete($contentId, $this->contentDocumentTypeIdentifier);
244
        }
245
246
        // 2. Delete all Content's Locations
247
        $this->gateway->deleteByQuery(json_encode(['query' => ['match' => ['content_id_id' => $contentId]]]), $this->locationDocumentTypeIdentifier);
248
    }
249
250
    /**
251
     * Deletes a location from the index.
252
     *
253
     * @todo When we support Location-less Content, we will have to reindex instead of removing
254
     * @todo Should we not already support the above?
255
     * @todo The subtree could potentially be huge, so this implementation should scroll reindex
256
     *
257
     * @param mixed $locationId
258
     * @param mixed $contentId @todo Make use of this, or remove if not needed.
259
     */
260
    public function deleteLocation($locationId, $contentId)
261
    {
262
        // 1. Update (reindex) all Content in the subtree with additional Location(s) outside of it
263
        $ast = array(
264
            'filter' => array(
265
                'and' => array(
266
                    0 => array(
267
                        'nested' => array(
268
                            'path' => 'locations_doc',
269
                            'filter' => array(
270
                                'regexp' => array(
271
                                    'locations_doc.path_string_id' => ".*/{$locationId}/.*",
272
                                ),
273
                            ),
274
                        ),
275
                    ),
276
                    1 => array(
277
                        'nested' => array(
278
                            'path' => 'locations_doc',
279
                            'filter' => array(
280
                                'regexp' => array(
281
                                    'locations_doc.path_string_id' => array(
282
                                        // Matches anything (@) and (&) not (~) <expression>
283
                                        'value' => "@&~(.*/{$locationId}/.*)",
284
                                        'flags' => 'INTERSECTION|COMPLEMENT|ANYSTRING',
285
                                    ),
286
                                ),
287
                            ),
288
                        ),
289
                    ),
290
                ),
291
            ),
292
        );
293
294
        $response = $this->gateway->findRaw(json_encode($ast), $this->contentDocumentTypeIdentifier);
295
        $result = json_decode($response->body);
296
297
        $documents = array();
298
        foreach ($result->hits->hits as $hit) {
299
            $documents[] = $this->mapper->mapContentById($hit->_id);
300
        }
301
302
        $this->gateway->bulkIndex($documents);
303
304
        // 2. Delete all Content in the subtree with no other Location(s) outside of it
305
        $ast['filter']['and'][1] = array(
306
            'not' => $ast['filter']['and'][1],
307
        );
308
        $ast = array(
309
            'query' => array(
310
                'filtered' => $ast,
311
            ),
312
        );
313
314
        $response = $this->gateway->findRaw(json_encode($ast), $this->contentDocumentTypeIdentifier);
315
        $documentsToDelete = json_decode($response->body);
316
317
        foreach ($documentsToDelete->hits->hits as $documentToDelete) {
318
            $this->gateway->deleteByQuery(json_encode(['query' => ['match' => ['_id' => $documentToDelete->_id]]]), $this->contentDocumentTypeIdentifier);
319
            $this->gateway->deleteByQuery(json_encode(['query' => ['match' => ['content_id_id' => $documentToDelete->_id]]]), $this->locationDocumentTypeIdentifier);
320
        }
321
    }
322
323
    /**
324
     * Purges all contents from the index.
325
     */
326
    public function purgeIndex()
327
    {
328
        $this->gateway->purgeIndex($this->contentDocumentTypeIdentifier);
329
        $this->gateway->purgeIndex($this->locationDocumentTypeIdentifier);
330
    }
331
332
    /**
333
     * Set if index/delete actions should commit or if several actions is to be expected.
334
     *
335
     * This should be set to false before group of actions and true before the last one
336
     *
337
     * @param bool $commit
338
     */
339
    public function setCommit($commit)
0 ignored issues
show
Unused Code introduced by
The parameter $commit is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
340
    {
341
        //$this->gateway->setCommit( $commit );
342
    }
343
344
    public function flush()
345
    {
346
        $this->gateway->flush();
347
    }
348
349
    public function supports($capabilityFlag)
350
    {
351
        return false;
352
    }
353
}
354