Completed
Push — master ( 97e40f...89ec5c )
by André
40:26 queued 12:35
created

SearchService::suggest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 4
dl 0
loc 3
rs 10
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\PermissionCriterionResolver;
13
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
14
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd;
15
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalOperator;
16
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Location as LocationCriterion;
17
use eZ\Publish\API\Repository\Values\Content\Query\SortClause\Location as LocationSortClause;
18
use eZ\Publish\API\Repository\Values\Content\Query;
19
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
20
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
21
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
22
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
23
use eZ\Publish\API\Repository\Exceptions\UnauthorizedException as APIUnauthorizedException;
24
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
25
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
26
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
27
use eZ\Publish\SPI\Search\Capable;
28
use eZ\Publish\Core\Search\Common\BackgroundIndexer;
29
use eZ\Publish\SPI\Search\Handler;
30
31
/**
32
 * Search service.
33
 */
34
class SearchService implements SearchServiceInterface
35
{
36
    /**
37
     * @var \eZ\Publish\Core\Repository\Repository
38
     */
39
    protected $repository;
40
41
    /**
42
     * @var \eZ\Publish\SPI\Search\Handler
43
     */
44
    protected $searchHandler;
45
46
    /**
47
     * @var array
48
     */
49
    protected $settings;
50
51
    /**
52
     * @var \eZ\Publish\Core\Repository\Helper\DomainMapper
53
     */
54
    protected $domainMapper;
55
56
    /**
57
     * @var \eZ\Publish\API\Repository\PermissionCriterionResolver
58
     */
59
    protected $permissionCriterionResolver;
60
61
    /**
62
     * @var \eZ\Publish\Core\Search\Common\BackgroundIndexer
63
     */
64
    protected $backgroundIndexer;
65
66
    /**
67
     * Setups service with reference to repository object that created it & corresponding handler.
68
     *
69
     * @param \eZ\Publish\API\Repository\Repository $repository
70
     * @param \eZ\Publish\SPI\Search\Handler $searchHandler
71
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
72
     * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
73
     * @param \eZ\Publish\Core\Search\Common\BackgroundIndexer $backgroundIndexer
74
     * @param array $settings
75
     */
76
    public function __construct(
77
        RepositoryInterface $repository,
78
        Handler $searchHandler,
79
        Helper\DomainMapper $domainMapper,
80
        PermissionCriterionResolver $permissionCriterionResolver,
81
        BackgroundIndexer $backgroundIndexer,
82
        array $settings = array()
83
    ) {
84
        $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...
85
        $this->searchHandler = $searchHandler;
86
        $this->domainMapper = $domainMapper;
87
        // Union makes sure default settings are ignored if provided in argument
88
        $this->settings = $settings + array(
89
            //'defaultSetting' => array(),
90
        );
91
        $this->permissionCriterionResolver = $permissionCriterionResolver;
92
        $this->backgroundIndexer = $backgroundIndexer;
93
    }
94
95
    /**
96
     * Finds content objects for the given query.
97
     *
98
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
99
     *
100
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
101
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
102
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
103
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
104
     * @param bool $filterOnUserPermissions if true only the objects which the user is allowed to read are returned.
105
     *
106
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
107
     */
108
    public function findContent(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
109
    {
110
        $contentService = $this->repository->getContentService();
111
        $result = $this->internalFindContentInfo($query, $languageFilter, $filterOnUserPermissions);
112
        foreach ($result->searchHits as $key => $hit) {
113
            try {
114
                // As we get ContentInfo from SPI, we need to load full content (avoids getting stale content data)
115
                $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...
116
                    $hit->valueObject->id,
117
                    (!empty($languageFilter['languages']) ? $languageFilter['languages'] : null),
118
                    null,
119
                    false,
120
                    (isset($languageFilter['useAlwaysAvailable']) ? $languageFilter['useAlwaysAvailable'] : true)
121
                );
122
            } catch (APINotFoundException $e) {
123
                // Most likely stale data, so we register content for background re-indexing.
124
                $this->backgroundIndexer->registerContent($hit->valueObject);
125
                unset($result->searchHits[$key]);
126
                --$result->totalCount;
127
            } catch (APIUnauthorizedException $e) {
128
                // Most likely stale cached permission criterion, as ttl is only a few seconds we don't react to this
129
                unset($result->searchHits[$key]);
130
                --$result->totalCount;
131
            }
132
        }
133
134
        return $result;
135
    }
136
137
    /**
138
     * Finds contentInfo objects for the given query.
139
     *
140
     * @see SearchServiceInterface::findContentInfo()
141
     *
142
     * @since 5.4.5
143
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
144
     *
145
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
146
     * @param array $languageFilter - a map of filters for the returned fields.
147
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
148
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
149
     * @param bool $filterOnUserPermissions if true (default) only the objects which is the user allowed to read are returned.
150
     *
151
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
152
     */
153
    public function findContentInfo(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
154
    {
155
        $result = $this->internalFindContentInfo($query, $languageFilter, $filterOnUserPermissions);
156
        foreach ($result->searchHits as $hit) {
157
            $hit->valueObject = $this->domainMapper->buildContentInfoDomainObject(
158
                $hit->valueObject
159
            );
160
        }
161
162
        return $result;
163
    }
164
165
    /**
166
     * Finds SPI content info objects for the given query.
167
     *
168
     * Internal for use by {@link findContent} and {@link findContentInfo}.
169
     *
170
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
171
     *
172
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
173
     * @param array $languageFilter - a map of filters for the returned fields.
174
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
175
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
176
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
177
     *
178
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult With "raw" SPI contentInfo objects in result
179
     */
180
    protected function internalFindContentInfo(Query $query, array $languageFilter = array(), $filterOnUserPermissions = true)
181
    {
182
        if (!is_int($query->offset)) {
183
            throw new InvalidArgumentType(
184
                '$query->offset',
185
                'integer',
186
                $query->offset
187
            );
188
        }
189
190
        if (!is_int($query->limit)) {
191
            throw new InvalidArgumentType(
192
                '$query->limit',
193
                'integer',
194
                $query->limit
195
            );
196
        }
197
198
        $query = clone $query;
199
        $query->filter = $query->filter ?: new Criterion\MatchAll();
200
201
        $this->validateContentCriteria(array($query->query), '$query');
202
        $this->validateContentCriteria(array($query->filter), '$query');
203
        $this->validateContentSortClauses($query);
204
205 View Code Duplication
        if ($filterOnUserPermissions && !$this->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...
206
            return new SearchResult(array('time' => 0, 'totalCount' => 0));
207
        }
208
209
        return $this->searchHandler->findContent($query, $languageFilter);
210
    }
211
212
    /**
213
     * Checks that $criteria does not contain Location criterions.
214
     *
215
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
216
     *
217
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion[] $criteria
218
     * @param string $argumentName
219
     */
220
    protected function validateContentCriteria(array $criteria, $argumentName)
221
    {
222
        foreach ($criteria as $criterion) {
223
            if ($criterion instanceof LocationCriterion) {
224
                throw new InvalidArgumentException(
225
                    $argumentName,
226
                    'Location criterions cannot be used in Content search'
227
                );
228
            }
229
            if ($criterion instanceof LogicalOperator) {
230
                $this->validateContentCriteria($criterion->criteria, $argumentName);
231
            }
232
        }
233
    }
234
235
    /**
236
     * Checks that $query does not contain Location sort clauses.
237
     *
238
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
239
     *
240
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
241
     */
242
    protected function validateContentSortClauses(Query $query)
243
    {
244
        foreach ($query->sortClauses as $sortClause) {
245
            if ($sortClause instanceof LocationSortClause) {
246
                throw new InvalidArgumentException('$query', 'Location sort clauses cannot be used in Content search');
247
            }
248
        }
249
    }
250
251
    /**
252
     * Performs a query for a single content object.
253
     *
254
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the object was not found by the query or due to permissions
255
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if criterion is not valid
256
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is more than one result matching the criterions
257
     *
258
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
259
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
260
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
261
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
262
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
263
     *
264
     * @return \eZ\Publish\API\Repository\Values\Content\Content
265
     */
266
    public function findSingle(Criterion $filter, array $languageFilter = array(), $filterOnUserPermissions = true)
267
    {
268
        $this->validateContentCriteria(array($filter), '$filter');
269
270
        if ($filterOnUserPermissions && !$this->addPermissionsCriterion($filter)) {
271
            throw new NotFoundException('Content', '*');
272
        }
273
274
        $contentInfo = $this->searchHandler->findSingle($filter, $languageFilter);
275
276
        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...
277
            $contentInfo->id,
278
            (!empty($languageFilter['languages']) ? $languageFilter['languages'] : null),
279
            null,
280
            false,
281
            (isset($languageFilter['useAlwaysAvailable']) ? $languageFilter['useAlwaysAvailable'] : true)
282
        );
283
    }
284
285
    /**
286
     * Suggests a list of values for the given prefix.
287
     *
288
     * @param string $prefix
289
     * @param string[] $fieldPaths
290
     * @param int $limit
291
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
292
     */
293
    public function suggest($prefix, $fieldPaths = array(), $limit = 10, Criterion $filter = null)
294
    {
295
    }
296
297
    /**
298
     * Finds Locations for the given query.
299
     *
300
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if query is not valid
301
     *
302
     * @param \eZ\Publish\API\Repository\Values\Content\LocationQuery $query
303
     * @param array $languageFilter Configuration for specifying prioritized languages query will be performed on.
304
     *        Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code>
305
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations
306
     * @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned.
307
     *
308
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
309
     */
310
    public function findLocations(LocationQuery $query, array $languageFilter = array(), $filterOnUserPermissions = true)
311
    {
312
        if (!is_int($query->offset)) {
313
            throw new InvalidArgumentType(
314
                '$query->offset',
315
                'integer',
316
                $query->offset
317
            );
318
        }
319
320
        if (!is_int($query->limit)) {
321
            throw new InvalidArgumentType(
322
                '$query->limit',
323
                'integer',
324
                $query->limit
325
            );
326
        }
327
328
        $query = clone $query;
329
        $query->filter = $query->filter ?: new Criterion\MatchAll();
330
331 View Code Duplication
        if ($filterOnUserPermissions && !$this->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...
332
            return new SearchResult(array('time' => 0, 'totalCount' => 0));
333
        }
334
335
        $result = $this->searchHandler->findLocations($query, $languageFilter);
336
337
        $missingLocations = $this->domainMapper->buildLocationDomainObjectsOnSearchResult($result);
338
        foreach ($missingLocations as $missingLocation) {
339
            $this->backgroundIndexer->registerLocation($missingLocation);
340
        }
341
342
        return $result;
343
    }
344
345
    /**
346
     * Adds content, read Permission criteria if needed and return false if no access at all.
347
     *
348
     * @uses \eZ\Publish\API\Repository\PermissionCriterionResolver::getPermissionsCriterion()
349
     *
350
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
351
     *
352
     * @return bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
353
     */
354 View Code Duplication
    protected function addPermissionsCriterion(Criterion &$criterion)
355
    {
356
        $permissionCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'read');
357
        if ($permissionCriterion === true || $permissionCriterion === false) {
358
            return $permissionCriterion;
359
        }
360
361
        // Merge with original $criterion
362
        if ($criterion instanceof LogicalAnd) {
363
            $criterion->criteria[] = $permissionCriterion;
364
        } else {
365
            $criterion = new LogicalAnd(
366
                array(
0 ignored issues
show
Documentation introduced by
array($criterion, $permissionCriterion) is of type array<integer,object<eZ\...t\\Query\\Criterion>"}>, but the function expects a array<integer,object<eZ\...ntent\Query\Criterion>>.

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...
367
                    $criterion,
368
                    $permissionCriterion,
369
                )
370
            );
371
        }
372
373
        return true;
374
    }
375
376
    public function supports($capabilityFlag)
377
    {
378
        if ($this->searchHandler instanceof Capable) {
379
            return $this->searchHandler->supports($capabilityFlag);
380
        }
381
382
        return false;
383
    }
384
}
385