1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Oro\Bundle\AnalyticsBundle\Builder; |
4
|
|
|
|
5
|
|
|
use Doctrine\Common\Collections\Criteria; |
6
|
|
|
|
7
|
|
|
use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; |
8
|
|
|
use Oro\Bundle\BatchBundle\ORM\Query\BufferedQueryResultIterator; |
9
|
|
|
use Oro\Bundle\AnalyticsBundle\Model\RFMAwareInterface; |
10
|
|
|
use Oro\Bundle\ChannelBundle\Entity\Channel; |
11
|
|
|
use Oro\Bundle\AnalyticsBundle\Entity\RFMMetricCategory; |
12
|
|
|
|
13
|
|
|
class RFMBuilder implements AnalyticsBuilderInterface |
14
|
|
|
{ |
15
|
|
|
const BATCH_SIZE = 200; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var DoctrineHelper |
19
|
|
|
*/ |
20
|
|
|
protected $doctrineHelper; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var RFMProviderInterface[] |
24
|
|
|
*/ |
25
|
|
|
protected $providers = []; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var array categories by channel |
29
|
|
|
*/ |
30
|
|
|
protected $categories = []; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @param DoctrineHelper $doctrineHelper |
34
|
|
|
*/ |
35
|
|
|
public function __construct(DoctrineHelper $doctrineHelper) |
36
|
|
|
{ |
37
|
|
|
$this->doctrineHelper = $doctrineHelper; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @param RFMProviderInterface $provider |
42
|
|
|
*/ |
43
|
|
|
public function addProvider(RFMProviderInterface $provider) |
44
|
|
|
{ |
45
|
|
|
$type = $provider->getType(); |
46
|
|
|
|
47
|
|
|
if (!in_array($type, RFMMetricCategory::$types, true)) { |
48
|
|
|
throw new \InvalidArgumentException( |
49
|
|
|
sprintf('Expected one of "%s" type, "%s" given', implode(',', RFMMetricCategory::$types), $type) |
50
|
|
|
); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
$this->providers[] = $provider; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* {@inheritdoc} |
58
|
|
|
*/ |
59
|
|
|
public function supports(Channel $channel) |
60
|
|
|
{ |
61
|
|
|
return is_a($channel->getCustomerIdentity(), 'Oro\Bundle\AnalyticsBundle\Model\RFMAwareInterface', true); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* {@inheritdoc} |
66
|
|
|
*/ |
67
|
|
|
public function build(Channel $channel, array $ids = []) |
68
|
|
|
{ |
69
|
|
|
$data = $channel->getData(); |
70
|
|
|
if (empty($data[RFMAwareInterface::RFM_STATE_KEY]) |
71
|
|
|
|| !filter_var($data[RFMAwareInterface::RFM_STATE_KEY], FILTER_VALIDATE_BOOLEAN) |
72
|
|
|
) { |
73
|
|
|
return; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
$iterator = $this->getEntityIdsByChannel($channel, $ids); |
77
|
|
|
|
78
|
|
|
$values = []; |
79
|
|
|
$count = 0; |
80
|
|
|
foreach ($iterator as $value) { |
81
|
|
|
$values[] = $value; |
82
|
|
|
$count++; |
83
|
|
|
if ($count % self::BATCH_SIZE === 0) { |
84
|
|
|
$this->processBatch($channel, $values); |
85
|
|
|
$values = []; |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
$this->processBatch($channel, $values); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param Channel $channel |
93
|
|
|
* @param array $values |
94
|
|
|
*/ |
95
|
|
|
protected function processBatch(Channel $channel, array $values) |
96
|
|
|
{ |
97
|
|
|
$toUpdate = []; |
98
|
|
|
foreach ($this->providers as $provider) { |
99
|
|
|
if (!$provider->supports($channel)) { |
100
|
|
|
continue; |
101
|
|
|
} |
102
|
|
|
$providerValues = $provider->getValues($channel, array_map(function ($value) { |
103
|
|
|
return $value['id']; |
104
|
|
|
}, $values)); |
105
|
|
|
|
106
|
|
|
$type = $provider->getType(); |
107
|
|
|
|
108
|
|
|
foreach ($values as $value) { |
109
|
|
|
$metric = isset($providerValues[$value['id']]) ? $providerValues[$value['id']] : null; |
110
|
|
|
$index = $this->getIndex($channel, $type, $metric); |
111
|
|
|
if ($index !== $value[$type]) { |
112
|
|
|
$toUpdate[$value['id']][$type] = $index; |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
$this->updateValues($channel, $toUpdate); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @param Channel $channel |
121
|
|
|
* @param array $values |
122
|
|
|
* @throws \Doctrine\DBAL\ConnectionException |
123
|
|
|
* @throws \Exception |
124
|
|
|
*/ |
125
|
|
|
protected function updateValues(Channel $channel, array $values) |
126
|
|
|
{ |
127
|
|
|
if (count($values) === 0) { |
128
|
|
|
return; |
129
|
|
|
} |
130
|
|
|
$entityFQCN = $channel->getCustomerIdentity(); |
131
|
|
|
|
132
|
|
|
$em = $this->doctrineHelper->getEntityManager($entityFQCN); |
133
|
|
|
$idField = $this->doctrineHelper->getSingleEntityIdentifierFieldName($entityFQCN); |
134
|
|
|
$connection = $em->getConnection(); |
135
|
|
|
$connection->beginTransaction(); |
136
|
|
|
try { |
137
|
|
|
foreach ($values as $id => $value) { |
138
|
|
|
$qb = $em->createQueryBuilder(); |
139
|
|
|
$qb->update($entityFQCN, 'e'); |
140
|
|
|
foreach ($value as $metricName => $metricValue) { |
141
|
|
|
$qb->set('e.' . $metricName, ':' . $metricName); |
142
|
|
|
$qb->setParameter($metricName, $metricValue); |
143
|
|
|
} |
144
|
|
|
$qb->where($qb->expr()->eq('e.' . $idField, ':id')); |
145
|
|
|
$qb->setParameter('id', $id); |
146
|
|
|
$qb->getQuery()->execute(); |
147
|
|
|
} |
148
|
|
|
$connection->commit(); |
149
|
|
|
} catch (\Exception $e) { |
150
|
|
|
$connection->rollBack(); |
151
|
|
|
throw $e; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param Channel $channel |
157
|
|
|
* @param array $ids |
158
|
|
|
* @return \ArrayIterator|BufferedQueryResultIterator |
159
|
|
|
*/ |
160
|
|
|
protected function getEntityIdsByChannel(Channel $channel, array $ids = []) |
161
|
|
|
{ |
162
|
|
|
$entityFQCN = $channel->getCustomerIdentity(); |
163
|
|
|
|
164
|
|
|
$qb = $this->doctrineHelper->getEntityRepository($entityFQCN)->createQueryBuilder('e'); |
165
|
|
|
|
166
|
|
|
$metadata = $this->doctrineHelper->getEntityMetadataForClass($entityFQCN); |
167
|
|
|
$metrics = []; |
168
|
|
|
foreach ($this->providers as $provider) { |
169
|
|
|
if ($provider->supports($channel) && $metadata->hasField($provider->getType())) { |
170
|
|
|
$metrics[] = $provider->getType(); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
if (count($metrics) === 0) { |
175
|
|
|
return new \ArrayIterator(); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
$idField = sprintf('e.%s', $this->doctrineHelper->getSingleEntityIdentifierFieldName($entityFQCN)); |
179
|
|
|
$qb->select(preg_filter('/^/', 'e.', $metrics)) |
180
|
|
|
->addSelect($idField . ' as id') |
181
|
|
|
->where('e.dataChannel = :dataChannel') |
182
|
|
|
->orderBy($qb->expr()->asc($idField)) |
183
|
|
|
->setParameter('dataChannel', $channel); |
184
|
|
|
|
185
|
|
|
if (count($ids) !== 0) { |
186
|
|
|
$qb->andWhere($qb->expr()->in($idField, ':ids')) |
187
|
|
|
->setParameter('ids', $ids); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
return (new BufferedQueryResultIterator($qb))->setBufferSize(self::BATCH_SIZE); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @param Channel $channel |
195
|
|
|
* @param string $type |
196
|
|
|
* @param int $value |
197
|
|
|
* |
198
|
|
|
* @return int|null |
199
|
|
|
*/ |
200
|
|
|
protected function getIndex(Channel $channel, $type, $value) |
201
|
|
|
{ |
202
|
|
|
$channelId = $this->doctrineHelper->getSingleEntityIdentifier($channel); |
203
|
|
|
if (!$channelId) { |
204
|
|
|
return null; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$categories = $this->getCategories($channelId, $type); |
208
|
|
|
if (!$categories) { |
|
|
|
|
209
|
|
|
return null; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// null value must be ranked with worse index |
213
|
|
|
if ($value === null) { |
214
|
|
|
return array_pop($categories)->getCategoryIndex(); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
// Search for RFM category that match current value |
218
|
|
|
foreach ($categories as $category) { |
219
|
|
|
$maxValue = $category->getMaxValue(); |
220
|
|
|
if ($maxValue && $value > $maxValue) { |
221
|
|
|
continue; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
$minValue = $category->getMinValue(); |
225
|
|
|
if ($minValue !== null && $value <= $minValue) { |
226
|
|
|
continue; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
return $category->getCategoryIndex(); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
return null; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* @param int $channelId |
237
|
|
|
* @param string $type |
238
|
|
|
* |
239
|
|
|
* @return RFMMetricCategory[] |
240
|
|
|
*/ |
241
|
|
|
protected function getCategories($channelId, $type) |
242
|
|
|
{ |
243
|
|
|
if (array_key_exists($channelId, $this->categories) |
244
|
|
|
&& array_key_exists($type, $this->categories[$channelId]) |
245
|
|
|
) { |
246
|
|
|
return $this->categories[$channelId][$type]; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$categories = $this->doctrineHelper |
250
|
|
|
->getEntityRepository('OroAnalyticsBundle:RFMMetricCategory') |
251
|
|
|
->findBy(['channel' => $channelId, 'categoryType' => $type], ['categoryIndex' => Criteria::ASC]); |
252
|
|
|
|
253
|
|
|
$this->categories[$channelId][$type] = $categories; |
254
|
|
|
|
255
|
|
|
return $categories; |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.