Completed
Push — tolerant_search_service ( 768e04...a664cf )
by André
13:22
created

SearchService::findContent()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 4
nop 3
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\SearchService 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\Repository;
10
11
use eZ\Publish\API\Repository\SearchService as SearchServiceInterface;
12
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
13
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalOperator;
14
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Location as LocationCriterion;
15
use eZ\Publish\API\Repository\Values\Content\Query\SortClause\Location as LocationSortClause;
16
use eZ\Publish\API\Repository\Values\Content\Query;
17
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
18
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
19
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
20
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
21
use eZ\Publish\API\Repository\Exceptions\UnauthorizedException as APIUnauthorizedException;
22
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
23
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
24
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
25
use eZ\Publish\Core\Search\Common\BackgroundIndexer;
26
use eZ\Publish\SPI\Search\Handler;
27
28
/**
29
 * Search service.
30
 */
31
class SearchService implements SearchServiceInterface
32
{
33
    /**
34
     * @var \eZ\Publish\Core\Repository\Repository
35
     */
36
    protected $repository;
37
38
    /**
39
     * @var \eZ\Publish\SPI\Search\Handler
40
     */
41
    protected $searchHandler;
42
43
    /**
44
     * @var array
45
     */
46
    protected $settings;
47
48
    /**
49
     * @var \eZ\Publish\Core\Repository\Helper\DomainMapper
50
     */
51
    protected $domainMapper;
52
53
    /**
54
     * @var \eZ\Publish\Core\Repository\PermissionsCriterionHandler
55
     */
56
    protected $permissionsCriterionHandler;
57
58
    /**
59
     * @var \eZ\Publish\Core\Search\Common\BackgroundIndexer
60
     */
61
    protected $backgroundIndexer;
62
63
    /**
64
     * Setups service with reference to repository object that created it & corresponding handler.
65
     *
66
     * @param \eZ\Publish\API\Repository\Repository $repository
67
     * @param \eZ\Publish\SPI\Search\Handler $searchHandler
68
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
69
     * @param \eZ\Publish\Core\Repository\PermissionsCriterionHandler $permissionsCriterionHandler
70
     * @param \eZ\Publish\Core\Search\Common\BackgroundIndexer $backgroundIndexer
71
     * @param array $settings
72
     */
73
    public function __construct(
74
        RepositoryInterface $repository,
75
        Handler $searchHandler,
76
        Helper\DomainMapper $domainMapper,
77
        PermissionsCriterionHandler $permissionsCriterionHandler,
78
        BackgroundIndexer $backgroundIndexer,
79
        array $settings = array()
80
    ) {
81
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
$repository is of type object<eZ\Publish\API\Repository\Repository>, but the property $repository was declared to be of type object<eZ\Publish\Core\Repository\Repository>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
82
        $this->searchHandler = $searchHandler;
83
        $this->domainMapper = $domainMapper;
84
        // Union makes sure default settings are ignored if provided in argument
85
        $this->settings = $settings + array(
86
            //'defaultSetting' => array(),
87
        );
88
        $this->permissionsCriterionHandler = $permissionsCriterionHandler;
89
        $this->backgroundIndexer = $backgroundIndexer;
90
    }
91
92
    /**
93
     * Finds content objects for the given query.
94
     *
95
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
96
     *
97
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
98
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
99
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
100
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
101
     * @param bool $filterOnUserPermissions if true only the objects which the user is allowed to read are returned.
102
     *
103
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
104
     */
105
    public function findContent(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
106
    {
107
        $contentService = $this->repository->getContentService();
108
        $result = $this->internalFindContentInfo($query, $languageFilter, $filterOnUserPermissions);
109
        foreach ($result->searchHits as $key => $hit) {
110
            try {
111
                // As we get ContentInfo from SPI, we need to load full content (avoids getting stale content data)
112
                $hit->valueObject = $contentService->internalLoadContent(
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
113
                    $hit->valueObject->id,
114
                    (!empty($languageFilter['languages']) ? $languageFilter['languages'] : null),
115
                    null,
116
                    false,
117
                    (isset($languageFilter['useAlwaysAvailable']) ? $languageFilter['useAlwaysAvailable'] : true)
118
                );
119
            } catch (APINotFoundException $e) {
120
                // Most likely stale data, so we register content for background re-indexing.
121
                $this->backgroundIndexer->registerForRefresh($hit->valueObject);
122
                unset($result->searchHits[$key]);
123
                --$result->totalCount;
124
            } catch (APIUnauthorizedException $e) {
125
                // Most likely stale cached permission criterion, as ttl is only a few seconds we don't react to this
126
                unset($result->searchHits[$key]);
127
                --$result->totalCount;
128
            }
129
        }
130
131
        return $result;
132
    }
133
134
    /**
135
     * Finds contentInfo objects for the given query.
136
     *
137
     * @see SearchServiceInterface::findContentInfo()
138
     *
139
     * @since 5.4.5
140
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
141
     *
142
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
143
     * @param array $languageFilter - a map of filters for the returned fields.
144
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
145
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
146
     * @param bool $filterOnUserPermissions if true (default) only the objects which is the user allowed to read are returned.
147
     *
148
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
149
     */
150
    public function findContentInfo(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
151
    {
152
        $result = $this->internalFindContentInfo($query, $languageFilter, $filterOnUserPermissions);
153
        foreach ($result->searchHits as $hit) {
154
            $hit->valueObject = $this->domainMapper->buildContentInfoDomainObject(
155
                $hit->valueObject
156
            );
157
        }
158
159
        return $result;
160
    }
161
162
    /**
163
     * Finds SPI content info objects for the given query.
164
     *
165
     * Internal for use by {@link findContent} and {@link findContentInfo}.
166
     *
167
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
168
     *
169
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
170
     * @param array $languageFilter - a map of filters for the returned fields.
171
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
172
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
173
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
174
     *
175
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult With "raw" SPI contentInfo objects in result
176
     */
177
    protected function internalFindContentInfo(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
178
    {
179
        if (!is_int($query->offset)) {
180
            throw new InvalidArgumentType(
181
                '$query->offset',
182
                'integer',
183
                $query->offset
184
            );
185
        }
186
187
        if (!is_int($query->limit)) {
188
            throw new InvalidArgumentType(
189
                '$query->limit',
190
                'integer',
191
                $query->limit
192
            );
193
        }
194
195
        $query = clone $query;
196
        $query->filter = $query->filter ?: new Criterion\MatchAll();
197
198
        $this->validateContentCriteria(array($query->query), '$query');
199
        $this->validateContentCriteria(array($query->filter), '$query');
200
        $this->validateContentSortClauses($query);
201
202 View Code Duplication
        if ($filterOnUserPermissions && !$this->permissionsCriterionHandler->addPermissionsCriterion($query->filter)) {
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...
203
            return new SearchResult(array('time' => 0, 'totalCount' => 0));
204
        }
205
206
        return $this->searchHandler->findContent($query, $languageFilter);
207
    }
208
209
    /**
210
     * Checks that $criteria does not contain Location criterions.
211
     *
212
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
213
     *
214
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion[] $criteria
215
     * @param string $argumentName
216
     */
217
    protected function validateContentCriteria(array $criteria, $argumentName)
218
    {
219
        foreach ($criteria as $criterion) {
220
            if ($criterion instanceof LocationCriterion) {
221
                throw new InvalidArgumentException(
222
                    $argumentName,
223
                    'Location criterions cannot be used in Content search'
224
                );
225
            }
226
            if ($criterion instanceof LogicalOperator) {
227
                $this->validateContentCriteria($criterion->criteria, $argumentName);
228
            }
229
        }
230
    }
231
232
    /**
233
     * Checks that $query does not contain Location sort clauses.
234
     *
235
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
236
     *
237
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
238
     */
239
    protected function validateContentSortClauses(Query $query)
240
    {
241
        foreach ($query->sortClauses as $sortClause) {
242
            if ($sortClause instanceof LocationSortClause) {
243
                throw new InvalidArgumentException('$query', 'Location sort clauses cannot be used in Content search');
244
            }
245
        }
246
    }
247
248
    /**
249
     * Performs a query for a single content object.
250
     *
251
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the object was not found by the query or due to permissions
252
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if criterion is not valid
253
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is more than one result matching the criterions
254
     *
255
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
256
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
257
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
258
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
259
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
260
     *
261
     * @return \eZ\Publish\API\Repository\Values\Content\Content
262
     */
263
    public function findSingle(Criterion $filter, array $languageFilter = array(), $filterOnUserPermissions = true)
264
    {
265
        $this->validateContentCriteria(array($filter), '$filter');
266
267
        if ($filterOnUserPermissions && !$this->permissionsCriterionHandler->addPermissionsCriterion($filter)) {
268
            throw new NotFoundException('Content', '*');
269
        }
270
271
        $contentInfo = $this->searchHandler->findSingle($filter, $languageFilter);
272
273
        return $this->repository->getContentService()->internalLoadContent(
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
274
            $contentInfo->id,
275
            (!empty($languageFilter['languages']) ? $languageFilter['languages'] : null),
276
            null,
277
            false,
278
            (isset($languageFilter['useAlwaysAvailable']) ? $languageFilter['useAlwaysAvailable'] : true)
279
        );
280
    }
281
282
    /**
283
     * Suggests a list of values for the given prefix.
284
     *
285
     * @param string $prefix
286
     * @param string[] $fieldPaths
287
     * @param int $limit
288
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
289
     */
290
    public function suggest($prefix, $fieldPaths = array(), $limit = 10, Criterion $filter = null)
291
    {
292
    }
293
294
    /**
295
     * Finds Locations for the given query.
296
     *
297
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
298
     *
299
     * @param \eZ\Publish\API\Repository\Values\Content\LocationQuery $query
300
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
301
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
302
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations
303
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
304
     *
305
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
306
     */
307
    public function findLocations(LocationQuery $query, array $languageFilter = array(), $filterOnUserPermissions = true)
308
    {
309
        if (!is_int($query->offset)) {
310
            throw new InvalidArgumentType(
311
                '$query->offset',
312
                'integer',
313
                $query->offset
314
            );
315
        }
316
317
        if (!is_int($query->limit)) {
318
            throw new InvalidArgumentType(
319
                '$query->limit',
320
                'integer',
321
                $query->limit
322
            );
323
        }
324
325
        $query = clone $query;
326
        $query->filter = $query->filter ?: new Criterion\MatchAll();
327
328 View Code Duplication
        if ($filterOnUserPermissions && !$this->permissionsCriterionHandler->addPermissionsCriterion($query->filter)) {
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...
329
            return new SearchResult(array('time' => 0, 'totalCount' => 0));
330
        }
331
332
        $result = $this->searchHandler->findLocations($query, $languageFilter);
333
334
        foreach ($result->searchHits as $key => $hit) {
335
            try {
336
                $hit->valueObject = $this->domainMapper->buildLocationDomainObject(
337
                    $hit->valueObject
338
                );
339
            } catch (APINotFoundException $e) {
340
                // Most likely stale data, so we register content for background re-indexing.
341
                $this->backgroundIndexer->registerForRefresh($hit->valueObject);
0 ignored issues
show
Documentation introduced by
$hit->valueObject is of type object<eZ\Publish\Core\R...alues\Content\Location>, but the function expects a object<eZ\Publish\SPI\Pe...tence\Content\Location>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
342
                unset($result->searchHits[$key]);
343
                --$result->totalCount;
344
            } catch (APIUnauthorizedException $e) {
345
                // Most likely stale cached permission criterion, as ttl is only a few seconds we don't react to this
346
                unset($result->searchHits[$key]);
347
                --$result->totalCount;
348
            }
349
        }
350
351
        return $result;
352
    }
353
}
354