CustomerSearchApiEntityManager   C
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 265
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 22
dl 0
loc 265
rs 5.7894
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B getSearchResult() 0 31 4
B mergeResults() 0 31 5
A getCustomerListQueryBuilder() 0 48 3
A getCustomerListFilters() 0 14 3
B getChannelFieldName() 0 25 6
A getCustomerEntities() 0 21 3
A getCustomerSearchAliases() 0 6 1
1
<?php
2
namespace Oro\Bundle\ChannelBundle\Entity\Manager;
3
4
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
5
6
use Doctrine\Common\Collections\Criteria;
7
use Doctrine\Common\Persistence\ObjectManager;
8
use Doctrine\ORM\EntityManager;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
11
use Oro\Bundle\EntityBundle\ORM\QueryUtils;
12
use Oro\Bundle\EntityBundle\ORM\SqlQueryBuilder;
13
use Oro\Bundle\SearchBundle\Engine\Indexer as SearchIndexer;
14
use Oro\Bundle\SearchBundle\Query\Result as SearchResult;
15
use Oro\Bundle\SearchBundle\Query\Result\Item as SearchResultItem;
16
use Oro\Bundle\SearchBundle\Event\PrepareResultItemEvent;
17
use Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager;
18
19
class CustomerSearchApiEntityManager extends ApiEntityManager
20
{
21
    const DEFAULT_CHANNEL_FIELD_NAME = 'dataChannel';
22
23
    const CHANNEL_ENTITY_CLASS = 'Oro\Bundle\ChannelBundle\Entity\Channel';
24
25
    const CUSTOMER_IDENTITY_INTERFACE = 'Oro\Bundle\ChannelBundle\Model\CustomerIdentityInterface';
26
27
    /** @var SearchIndexer */
28
    protected $searchIndexer;
29
30
    /** @var EventDispatcherInterface */
31
    protected $dispatcher;
32
33
    /**
34
     * {@inheritdoc}
35
     * @param SearchIndexer            $searchIndexer
36
     * @param EventDispatcherInterface $dispatcher
37
     */
38
    public function __construct(
39
        $class,
40
        ObjectManager $om,
41
        SearchIndexer $searchIndexer,
42
        EventDispatcherInterface $dispatcher
43
    ) {
44
        parent::__construct($class, $om);
45
        $this->searchIndexer = $searchIndexer;
46
        $this->dispatcher   = $dispatcher;
47
    }
48
49
    /**
50
     * Gets search result
51
     *
52
     * @param int           $page   Page number
53
     * @param int           $limit  Number of items per page
54
     * @param string        $search The search string.
55
     * @param Criteria|null $criteria
56
     *
57
     * @return array
58
     */
59
    public function getSearchResult($page = 1, $limit = 10, $search = '', $criteria = null)
60
    {
61
        $searchQuery = $this->searchIndexer->getSimpleSearchQuery(
62
            $search,
63
            $this->getOffset($page, $limit),
64
            $limit,
65
            $this->getCustomerSearchAliases()
0 ignored issues
show
Documentation introduced by
$this->getCustomerSearchAliases() is of type array<integer,string>, but the function expects a string|null.

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...
66
        );
67
68
        if ($criteria && $expression = $criteria->getWhereExpression()) {
69
            $searchQuery->getCriteria()->andWhere($expression);
70
        }
71
72
        $searchResult = $this->searchIndexer->query($searchQuery);
73
74
        $result = [
75
            'result'     => [],
76
            'totalCount' =>
77
                function () use ($searchResult) {
78
                    return $searchResult->getRecordsCount();
79
                }
80
        ];
81
82
        if ($searchResult->count() > 0) {
83
            $customers = $this->getCustomerListQueryBuilder($searchResult)->getQuery()->getResult();
84
85
            $result['result'] = $this->mergeResults($searchResult, $customers);
86
        }
87
88
        return $result;
89
    }
90
91
    /**
92
     * Merges the search result and customers
93
     *
94
     * @param SearchResult $searchResult
95
     * @param array        $customers
96
     *
97
     * @return array
98
     */
99
    protected function mergeResults(SearchResult $searchResult, array $customers)
100
    {
101
        $result = [];
102
103
        /** @var SearchResultItem $item */
104
        foreach ($searchResult as $item) {
105
            $this->dispatcher->dispatch(PrepareResultItemEvent::EVENT_NAME, new PrepareResultItemEvent($item));
106
107
            $id        = (int)$item->getRecordId();
108
            $className = $item->getEntityName();
109
110
            $resultItem = [
111
                'id'      => $id,
112
                'entity'  => $className,
113
                'title'   => $item->getRecordTitle(),
114
                'channel' => null
115
            ];
116
117
            foreach ($customers as $customer) {
118
                if ($customer['entity'] === $className && $customer['id'] === $id) {
119
                    $resultItem['channel'] = $customer['channelId'];
120
                    $resultItem['accountName'] = $customer['accountName'];
121
                    break;
122
                }
123
            }
124
125
            $result[] = $resultItem;
126
        }
127
128
        return $result;
129
    }
130
131
    /**
132
     * Returns a query builder that could be used for fetching the list of the Customer entities
133
     * filtered by ids.
134
     *
135
     * @param SearchResult $searchResult
136
     *
137
     * @return SqlQueryBuilder
138
     */
139
    protected function getCustomerListQueryBuilder(SearchResult $searchResult)
140
    {
141
        /** @var EntityManager $em */
142
        $em = $this->getObjectManager();
143
144
        $selectStmt = null;
145
        $subQueries = [];
146
        foreach ($this->getCustomerListFilters($searchResult) as $customerClass => $customerIds) {
147
            $subQb = $em->getRepository($customerClass)->createQueryBuilder('e')
148
                ->select(
149
                    sprintf(
150
                        'channel.id AS channelId, e.id AS entityId, \'%s\' AS entityClass, account.name as accountName',
151
                        str_replace('\'', '\'\'', $customerClass)
152
                    )
153
                )
154
                ->innerJoin('e.' . $this->getChannelFieldName($customerClass), 'channel')
155
                ->leftJoin('e.account', 'account');
156
            $subQb->where($subQb->expr()->in('e.id', $customerIds));
157
158
            $subQuery = $subQb->getQuery();
159
160
            $subQueries[] = QueryUtils::getExecutableSql($subQuery);
161
162
            if (empty($selectStmt)) {
163
                $mapping    = QueryUtils::parseQuery($subQuery)->getResultSetMapping();
164
                $selectStmt = sprintf(
165
                    'entity.%s AS channelId, entity.%s as entityId, entity.%s AS entityClass, entity.%s as accountName',
166
                    QueryUtils::getColumnNameByAlias($mapping, 'channelId'),
0 ignored issues
show
Bug introduced by
It seems like $mapping defined by \Oro\Bundle\EntityBundle...->getResultSetMapping() on line 163 can be null; however, Oro\Component\DoctrineUt...:getColumnNameByAlias() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
167
                    QueryUtils::getColumnNameByAlias($mapping, 'entityId'),
0 ignored issues
show
Bug introduced by
It seems like $mapping defined by \Oro\Bundle\EntityBundle...->getResultSetMapping() on line 163 can be null; however, Oro\Component\DoctrineUt...:getColumnNameByAlias() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
168
                    QueryUtils::getColumnNameByAlias($mapping, 'entityClass'),
0 ignored issues
show
Bug introduced by
It seems like $mapping defined by \Oro\Bundle\EntityBundle...->getResultSetMapping() on line 163 can be null; however, Oro\Component\DoctrineUt...:getColumnNameByAlias() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
169
                    QueryUtils::getColumnNameByAlias($mapping, 'accountName')
0 ignored issues
show
Bug introduced by
It seems like $mapping defined by \Oro\Bundle\EntityBundle...->getResultSetMapping() on line 163 can be null; however, Oro\Component\DoctrineUt...:getColumnNameByAlias() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
170
                );
171
            }
172
        }
173
174
        $rsm = QueryUtils::createResultSetMapping($em->getConnection()->getDatabasePlatform());
175
        $rsm
176
            ->addScalarResult('channelId', 'channelId', 'integer')
177
            ->addScalarResult('entityId', 'id', 'integer')
178
            ->addScalarResult('entityClass', 'entity')
179
            ->addScalarResult('accountName', 'accountName');
180
        $qb = new SqlQueryBuilder($em, $rsm);
0 ignored issues
show
Deprecated Code introduced by
The class Oro\Bundle\EntityBundle\ORM\SqlQueryBuilder has been deprecated with message: since 1.9. Use {@see Oro\Component\DoctrineUtils\ORM\SqlQueryBuilder} instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
181
        $qb
182
            ->select($selectStmt)
183
            ->from('(' . implode(' UNION ALL ', $subQueries) . ')', 'entity');
184
185
        return $qb;
186
    }
187
188
    /**
189
     * Extracts ids of the Customer entities from a given search result
190
     *
191
     * @param SearchResult $searchResult
192
     *
193
     * @return array example: ['Acme\Entity\Customer' => [1, 2, 3], ...]
194
     */
195
    protected function getCustomerListFilters(SearchResult $searchResult)
196
    {
197
        $filters = [];
198
        /** @var SearchResultItem $item */
199
        foreach ($searchResult as $item) {
200
            $entityClass = $item->getEntityName();
201
            if (!isset($filters[$entityClass])) {
202
                $filters[$entityClass] = [];
203
            }
204
            $filters[$entityClass][] = $item->getRecordId();
205
        }
206
207
        return $filters;
208
    }
209
210
    /**
211
     * Gets the field name for many-to-one relation between the Customer the Channel entities
212
     *
213
     * @param string $customerClass The FQCN of the Customer entity
214
     *
215
     * @return string
216
     *
217
     * @throws \RuntimeException if the relation not found
218
     */
219
    protected function getChannelFieldName($customerClass)
220
    {
221
        /** @var ClassMetadata $metadata */
222
        $metadata = $this->getObjectManager()->getClassMetadata($customerClass);
223
        if ($metadata->hasAssociation(self::DEFAULT_CHANNEL_FIELD_NAME)
224
            && $metadata->getAssociationTargetClass(self::DEFAULT_CHANNEL_FIELD_NAME) === self::CHANNEL_ENTITY_CLASS
225
        ) {
226
            return self::DEFAULT_CHANNEL_FIELD_NAME;
227
        }
228
229
        $channelAssociations = $metadata->getAssociationsByTargetClass(self::CHANNEL_ENTITY_CLASS);
230
        foreach ($channelAssociations as $fieldName => $mapping) {
231
            if ($mapping['type'] === ClassMetadata::MANY_TO_ONE && $mapping['isOwningSide']) {
232
                return $fieldName;
233
            }
234
        }
235
236
        throw new \RuntimeException(
237
            sprintf(
238
                'The entity "%s" must have many-to-one relation to "%s".',
239
                $customerClass,
240
                self::CHANNEL_ENTITY_CLASS
241
            )
242
        );
243
    }
244
245
    /**
246
     * Gets all class names for all the Customer entities
247
     *
248
     * @return string[]
249
     */
250
    protected function getCustomerEntities()
251
    {
252
        return array_map(
253
            function (ClassMetadata $metadata) {
254
                return $metadata->name;
255
            },
256
            array_filter(
257
                $this->getObjectManager()->getMetadataFactory()->getAllMetadata(),
258
                function (ClassMetadata $metadata) {
259
                    // @todo: should be removed in CRM-3263
260
                    if ($metadata->name === 'Oro\Bundle\ChannelBundle\Entity\CustomerIdentity') {
261
                        return false;
262
                    }
263
264
                    return
265
                        !$metadata->isMappedSuperclass
266
                        && $metadata->getReflectionClass()->isSubclassOf(self::CUSTOMER_IDENTITY_INTERFACE);
267
                }
268
            )
269
        );
270
    }
271
272
    /**
273
     * Returns search aliases for all the Customer entities
274
     *
275
     * @return string[]
276
     */
277
    protected function getCustomerSearchAliases()
278
    {
279
        return array_values(
280
            $this->searchIndexer->getEntityAliases($this->getCustomerEntities())
281
        );
282
    }
283
}
284