Completed
Push — master ( 664fb1...60ffba )
by
unknown
26:32
created

RFMBuilder::getEntityIdsByChannel()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
cc 6
eloc 20
nc 9
nop 2
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $categories of type Oro\Bundle\AnalyticsBund...ity\RFMMetricCategory[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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