Completed
Pull Request — develop (#177)
by Wachter
11:57
created

ArticleDataProvider::getAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
/*
4
 * This file is part of Sulu.
5
 *
6
 * (c) MASSIVE ART WebServices GmbH
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Sulu\Bundle\ArticleBundle\Content;
13
14
use ONGR\ElasticsearchBundle\Service\Manager;
15
use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
16
use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
17
use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
18
use ONGR\ElasticsearchDSL\Search;
19
use ONGR\ElasticsearchDSL\Sort\FieldSort;
20
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
21
use ProxyManager\Proxy\LazyLoadingInterface;
22
use Sulu\Bundle\ArticleBundle\Document\ArticleDocument;
23
use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocumentInterface;
24
use Sulu\Bundle\WebsiteBundle\ReferenceStore\ReferenceStoreInterface;
25
use Sulu\Component\Content\Compat\PropertyParameter;
26
use Sulu\Component\DocumentManager\DocumentManagerInterface;
27
use Sulu\Component\SmartContent\AliasDataProviderInterface;
28
use Sulu\Component\SmartContent\Configuration\Builder;
29
use Sulu\Component\SmartContent\Configuration\BuilderInterface;
30
use Sulu\Component\SmartContent\DataProviderInterface;
31
use Sulu\Component\SmartContent\DataProviderResult;
32
33
/**
34
 * Introduces articles in smart-content.
35
 */
36
class ArticleDataProvider implements DataProviderInterface, AliasDataProviderInterface
37
{
38
    /**
39
     * @var Manager
40
     */
41
    protected $searchManager;
42
43
    /**
44
     * @var DocumentManagerInterface
45
     */
46
    protected $documentManager;
47
48
    /**
49
     * @var LazyLoadingValueHolderFactory
50
     */
51
    protected $proxyFactory;
52
53
    /**
54
     * @var ReferenceStoreInterface
55
     */
56
    private $referenceStore;
57
58
    /**
59
     * @var string
60
     */
61
    protected $articleDocumentClass;
62
63
    /**
64
     * @var int
65
     */
66
    protected $defaultLimit;
67
68
    /**
69
     * @param Manager $searchManager
70
     * @param DocumentManagerInterface $documentManager
71
     * @param LazyLoadingValueHolderFactory $proxyFactory
72
     * @param ReferenceStoreInterface $referenceStore
73
     * @param string $articleDocumentClass
74
     * @param int $defaultLimit
75 15
     */
76
    public function __construct(
77
        Manager $searchManager,
78
        DocumentManagerInterface $documentManager,
79
        LazyLoadingValueHolderFactory $proxyFactory,
80
        ReferenceStoreInterface $referenceStore,
81
        $articleDocumentClass,
82
        $defaultLimit
83 15
    ) {
84 15
        $this->searchManager = $searchManager;
85 15
        $this->documentManager = $documentManager;
86 15
        $this->proxyFactory = $proxyFactory;
87 15
        $this->referenceStore = $referenceStore;
88 15
        $this->articleDocumentClass = $articleDocumentClass;
89 15
        $this->defaultLimit = $defaultLimit;
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function getConfiguration()
96
    {
97
        return $this->getConfigurationBuilder()->getConfiguration();
98
    }
99
100
    /**
101
     * Create new configuration-builder.
102
     *
103
     * @return BuilderInterface
104
     */
105
    protected function getConfigurationBuilder()
106
    {
107
        return Builder::create()
108
            ->enableTags()
109
            ->enableCategories()
110
            ->enableLimit()
111
            ->enablePagination()
112
            ->enablePresentAs()
113
            ->setDeepLink('article/{locale}/edit:{id}/details')
114
            ->enableSorting(
115
                [
116
                    ['column' => 'published', 'title' => 'sulu_article.smart-content.published'],
117
                    ['column' => 'authored', 'title' => 'sulu_article.smart-content.authored'],
118
                    ['column' => 'created', 'title' => 'sulu_article.smart-content.created'],
119
                    ['column' => 'title', 'title' => 'sulu_article.smart-content.title'],
120
                    ['column' => 'author_full_name', 'title' => 'sulu_article.smart-content.author-full-name'],
121
                ]
122
            );
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function getDefaultPropertyParameter()
129
    {
130
        return ['type' => new PropertyParameter('type', null)];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('type' => n...rameter('type', null)); (array<string,Sulu\Compon...mpat\PropertyParameter>) is incompatible with the return type declared by the interface Sulu\Component\SmartCont...efaultPropertyParameter of type Sulu\Component\Content\Compat\PropertyParameter[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
131
    }
132
133
    /**
134
     * {@inheritdoc}
135 11
     */
136
    public function resolveDataItems(
137
        array $filters,
138
        array $propertyParameter,
139
        array $options = [],
140
        $limit = null,
141
        $page = 1,
142
        $pageSize = null
143 11
    ) {
144 11
        $filters['types'] = $this->getTypesProperty($propertyParameter);
145
        $filters['excluded'] = $this->getExcludedFilter($filters, $propertyParameter);
146 11
147
        $queryResult = $this->getSearchResult($filters, $limit, $page, $pageSize, $options['locale']);
148 11
149 11
        $result = [];
150
        $uuids = [];
151 11
        /** @var ArticleViewDocumentInterface $document */
152 10
        foreach ($queryResult as $document) {
0 ignored issues
show
Bug introduced by
The expression $queryResult of type object<Countable> is not traversable.
Loading history...
153 10
            $uuids[] = $document->getUuid();
154
            $result[] = new ArticleDataItem($document->getUuid(), $document->getTitle(), $document);
155
        }
156 11
157
        return new DataProviderResult($result, $this->hasNextPage($queryResult, $limit, $page, $pageSize), $uuids);
158
    }
159
160
    /**
161
     * {@inheritdoc}
162 2
     */
163
    public function resolveResourceItems(
164
        array $filters,
165
        array $propertyParameter,
166
        array $options = [],
167
        $limit = null,
168
        $page = 1,
169
        $pageSize = null
170 2
    ) {
171 2
        $filters['types'] = $this->getTypesProperty($propertyParameter);
172
        $filters['excluded'] = $this->getExcludedFilter($filters, $propertyParameter);
173 2
174
        $queryResult = $this->getSearchResult($filters, $limit, $page, $pageSize, $options['locale']);
175 2
176 2
        $result = [];
177
        $uuids = [];
178 2
        /** @var ArticleViewDocumentInterface $document */
179 2
        foreach ($queryResult as $document) {
0 ignored issues
show
Bug introduced by
The expression $queryResult of type object<Countable> is not traversable.
Loading history...
180
            $this->referenceStore->add($document->getUuid());
181 2
182 2
            $uuids[] = $document->getUuid();
183
            $result[] = new ArticleResourceItem(
184 2
                $document,
185
                $this->getResource($document->getUuid(), $document->getLocale())
0 ignored issues
show
Documentation introduced by
$this->getResource($docu...$document->getLocale()) is of type object<ProxyManager\Proxy\VirtualProxyInterface>, but the function expects a object<Sulu\Bundle\Artic...cument\ArticleDocument>.

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...
186
            );
187
        }
188 2
189
        return new DataProviderResult($result, $this->hasNextPage($queryResult, $limit, $page, $pageSize), $uuids);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function resolveDatasource($datasource, array $propertyParameter, array $options)
196
    {
197
        return;
198
    }
199
200
    /**
201
     * Returns flag "hasNextPage".
202
     * It combines the limit/query-count with the page and page-size.
203
     *
204
     * @param \Countable $queryResult
205
     * @param int $limit
206
     * @param int $page
207
     * @param int $pageSize
208
     *
209
     * @return bool
210 13
     */
211
    private function hasNextPage(\Countable $queryResult, $limit, $page, $pageSize)
212 13
    {
213 13
        $count = $queryResult->count();
214 2
        if ($limit && $limit < $count) {
215
            $count = $limit;
216
        }
217 13
218
        return $count > ($page * $pageSize);
219
    }
220
221
    /**
222
     * Creates search for filters and returns search-result.
223
     *
224
     * @param array $filters
225
     * @param int $limit
226
     * @param int $page
227
     * @param int $pageSize
228
     * @param string $locale
229
     *
230
     * @return \Countable
231 13
     */
232
    private function getSearchResult(array $filters, $limit, $page, $pageSize, $locale)
233 13
    {
234 13
        $repository = $this->searchManager->getRepository($this->articleDocumentClass);
235 13
        $search = $this->createSearch($repository->createSearch(), $filters, $locale);
236
        if (!$search) {
237
            return new \ArrayIterator([]);
238
        }
239 13
240
        $this->addPagination($search, $pageSize, $page, $limit);
241 13
242
        if (array_key_exists('sortBy', $filters) && is_array($filters['sortBy'])) {
243
            $sortMethod = array_key_exists('sortMethod', $filters) ? $filters['sortMethod'] : 'asc';
244
            $this->appendSortBy($filters['sortBy'], $sortMethod, $search);
245
        }
246 13
247
        return $repository->findDocuments($search);
248
    }
249
250
    /**
251
     * Initialize search with neccesary queries.
252
     *
253
     * @param Search $search
254
     * @param array $filters
255
     * @param string $locale
256
     *
257
     * @return Search
258 12
     */
259
    protected function createSearch(Search $search, array $filters, $locale)
260 12
    {
261 2
        if (0 < count($filters['excluded'])) {
262 2
            foreach ($filters['excluded'] as $uuid) {
263
                $search->addQuery(new TermQuery('uuid', $uuid), BoolQuery::MUST_NOT);
264
            }
265
        }
266 12
267
        $query = new BoolQuery();
268 12
269 12
        $queriesCount = 0;
270 12
        $operator = $this->getFilter($filters, 'tagOperator', 'or');
271 12
        $this->addBoolQuery('tags', $filters, 'excerpt.tags.id', $operator, $query, $queriesCount);
272 12
        $operator = $this->getFilter($filters, 'websiteTagsOperator', 'or');
273
        $this->addBoolQuery('websiteTags', $filters, 'excerpt.tags.id', $operator, $query, $queriesCount);
274 12
275 12
        $operator = $this->getFilter($filters, 'categoryOperator', 'or');
276 12
        $this->addBoolQuery('categories', $filters, 'excerpt.categories.id', $operator, $query, $queriesCount);
277 12
        $operator = $this->getFilter($filters, 'websiteCategoriesOperator', 'or');
278
        $this->addBoolQuery('websiteCategories', $filters, 'excerpt.categories.id', $operator, $query, $queriesCount);
279 12
280 12
        if (null !== $locale) {
281
            $search->addQuery(new TermQuery('locale', $locale));
282
        }
283 12
284 3
        if (array_key_exists('types', $filters) && $filters['types']) {
285 3
            $typesQuery = new BoolQuery();
286 3
            foreach ($filters['types'] as $typeFilter) {
287
                $typesQuery->add(new TermQuery('type', $typeFilter), BoolQuery::SHOULD);
288 3
            }
289
            $search->addQuery($typesQuery);
290
        }
291 12
292 12
        if (0 === $queriesCount) {
293
            $search->addQuery(new MatchAllQuery(), BoolQuery::MUST);
294
        } else {
295
            $search->addQuery($query, BoolQuery::MUST);
296
        }
297 12
298
        return $search;
299
    }
300
301
    /**
302
     * Returns array with all types defined in property parameter.
303
     *
304
     * @param array $propertyParameter
305
     *
306
     * @return array
307 13
     */
308
    private function getTypesProperty($propertyParameter)
309 13
    {
310
        $filterTypes = [];
311 13
312 13
        if (array_key_exists('types', $propertyParameter)
313
            && null !== ($types = explode(',', $propertyParameter['types']->getValue()))
314 3
        ) {
315 3
            foreach ($types as $type) {
316
                $filterTypes[] = $type;
317
            }
318
        }
319 13
320
        return $filterTypes;
321
    }
322
323
    /**
324
     * Returns excluded articles.
325
     *
326
     * @param array $filters
327
     * @param PropertyParameter[] $propertyParameter
328
     *
329
     * @return array
330 13
     */
331
    private function getExcludedFilter(array $filters, array $propertyParameter)
332 13
    {
333 13
        $excluded = array_key_exists('excluded', $filters) ? $filters['excluded'] : [];
334 13
        if (array_key_exists('exclude_duplicates', $propertyParameter)
335
            && $propertyParameter['exclude_duplicates']->getValue()
336 1
        ) {
337
            $excluded = array_merge($excluded, $this->referenceStore->getAll());
338
        }
339 13
340
        return $excluded;
341
    }
342
343
    /**
344
     * Extension point to append order.
345
     *
346
     * @param array $sortBy
347
     * @param string $sortMethod
348
     * @param Search $search
349
     *
350
     * @return array parameters for query
351
     */
352
    private function appendSortBy($sortBy, $sortMethod, $search)
353
    {
354
        foreach ($sortBy as $column) {
355
            $search->addSort(new FieldSort($column, $sortMethod));
356
        }
357
    }
358
359
    /**
360
     * Add the pagination to given query.
361
     *
362
     * @param Search $search
363
     * @param int $pageSize
364
     * @param int $page
365
     * @param int $limit
366 13
     */
367
    private function addPagination(Search $search, $pageSize, $page, $limit)
368 13
    {
369 13
        $offset = 0;
370 4
        if ($pageSize) {
371 4
            $pageSize = intval($pageSize);
372
            $offset = ($page - 1) * $pageSize;
373
        }
374 13
375 11
        if ($limit === null) {
376
            $limit = $this->defaultLimit;
377
        }
378 13
379 11
        if ($pageSize === null || $offset + $pageSize > $limit) {
380
            $pageSize = $limit - $offset;
381 11
382
            if ($pageSize < 0) {
383
                $pageSize = 0;
384
            }
385
        }
386 13
387 13
        $search->setFrom($offset);
388 13
        $search->setSize($pageSize);
389
    }
390
391
    /**
392
     * Add a boolean-query if filter exists.
393
     *
394
     * @param string $filterName
395
     * @param array $filters
396
     * @param string $field
397
     * @param string $operator
398
     * @param BoolQuery $query
399
     * @param int $queriesCount
400 12
     */
401
    private function addBoolQuery($filterName, array $filters, $field, $operator, BoolQuery $query, &$queriesCount)
402 12
    {
403
        if (0 !== count($tags = $this->getFilter($filters, $filterName))) {
404
            ++$queriesCount;
405
            $query->add($this->getBoolQuery($field, $tags, $operator));
406 12
        }
407
    }
408
409
    /**
410
     * Returns boolean query for given fields and values.
411
     *
412
     * @param string $field
413
     * @param array $values
414
     * @param string $operator
415
     *
416
     * @return BoolQuery
417
     */
418
    private function getBoolQuery($field, array $values, $operator)
419
    {
420
        $type = ('or' === strtolower($operator) ? BoolQuery::SHOULD : BoolQuery::MUST);
421
422
        $query = new BoolQuery();
423
        foreach ($values as $value) {
424
            $query->add(new TermQuery($field, $value), $type);
425
        }
426
427
        return $query;
428
    }
429
430
    /**
431
     * Returns filter value.
432
     *
433
     * @param array $filters
434
     * @param string $name
435
     * @param mixed $default
436
     *
437
     * @return mixed
438 12
     */
439
    private function getFilter(array $filters, $name, $default = null)
440 12
    {
441
        if ($this->hasFilter($filters, $name)) {
442
            return $filters[$name];
443
        }
444 12
445
        return $default;
446
    }
447
448
    /**
449
     * Returns true if filter-value exists.
450
     *
451
     * @param array $filters
452
     * @param string $name
453
     *
454
     * @return bool
455 12
     */
456
    private function hasFilter(array $filters, $name)
457 12
    {
458
        return array_key_exists($name, $filters) && null !== $filters[$name];
459
    }
460
461
    /**
462
     * Returns Proxy document for uuid.
463
     *
464
     * @param string $uuid
465
     * @param string $locale
466
     *
467
     * @return object
468 2
     */
469
    private function getResource($uuid, $locale)
470 2
    {
471 2
        return $this->proxyFactory->createProxy(
472 2
            ArticleDocument::class,
473
            function (
474
                &$wrappedObject,
475
                LazyLoadingInterface $proxy,
476
                $method,
477
                array $parameters,
478
                &$initializer
479
            ) use ($uuid, $locale) {
480
                $initializer = null;
481
                $wrappedObject = $this->documentManager->find($uuid, $locale);
482
483 2
                return true;
484
            }
485
        );
486
    }
487
488
    /**
489
     * {@inheritdoc}
490
     */
491
    public function getAlias()
492
    {
493
        return 'article';
494
    }
495
}
496