Completed
Push — EZP-29891 ( 916cf6...0402ff )
by
unknown
16:53
created

SearchService   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 354
Duplicated Lines 12.71 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 45
loc 354
rs 9.1199
c 0
b 0
f 0
wmc 41
lcom 1
cbo 16

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 18 18 1
B findContent() 0 28 6
A findContentInfo() 0 11 2
B internalFindContentInfo() 3 31 6
A validateContentCriteria() 0 14 4
A validateContentSortClauses() 0 8 3
A findSingle() 0 18 5
A suggest() 0 3 1
B findLocations() 3 46 9
A addPermissionsCriterion() 21 21 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SearchService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SearchService, and based on these observations, apply Extract Interface, too.

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