Completed
Push — master ( 0bae06...931bd3 )
by
unknown
78:08
created

getRecordsToReset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 13
rs 9.4286
cc 2
eloc 8
nc 2
nop 3
1
<?php
2
3
namespace OroCRM\Bundle\ActivityContactBundle\Command;
4
5
use Doctrine\ORM\Query;
6
use Doctrine\ORM\QueryBuilder;
7
8
use Psr\Log\AbstractLogger;
9
10
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Output\OutputInterface;
13
use Symfony\Component\PropertyAccess\PropertyAccess;
14
15
use Oro\Component\PropertyAccess\PropertyAccessor;
16
use Oro\Component\Log\OutputLogger;
17
18
use Oro\Bundle\ActivityBundle\Event\ActivityEvent;
19
use Oro\Bundle\ActivityListBundle\Entity\ActivityList;
20
use Oro\Bundle\ActivityListBundle\Entity\Repository\ActivityListRepository;
21
use Oro\Bundle\ActivityListBundle\Filter\ActivityListFilterHelper;
22
use Oro\Bundle\ActivityListBundle\Tools\ActivityListEntityConfigDumperExtension;
23
use Oro\Bundle\EntityBundle\ORM\OroEntityManager;
24
use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface;
25
use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider;
26
use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper;
27
28
use OroCRM\Bundle\ActivityContactBundle\EntityConfig\ActivityScope;
29
use OroCRM\Bundle\ActivityContactBundle\EventListener\ActivityListener;
30
use OroCRM\Bundle\ActivityContactBundle\Provider\ActivityContactProvider;
31
use OroCRM\Bundle\ActivityContactBundle\Model\TargetExcludeList;
32
33
class ActivityContactRecalculateCommand extends ContainerAwareCommand
34
{
35
    const STATUS_SUCCESS = 0;
36
    const COMMAND_NAME   = 'oro:activity-contact:recalculate';
37
    const BATCH_SIZE     = 100;
38
39
    /** @var OroEntityManager $em */
40
    protected $em;
41
42
    /** @var ActivityListRepository $activityListRepository */
43
    protected $activityListRepository;
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    protected function configure()
49
    {
50
        $this
51
            ->setName(self::COMMAND_NAME)
52
            ->setDescription('Recalculate contacting activities');
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    protected function execute(InputInterface $input, OutputInterface $output)
59
    {
60
        $logger = new OutputLogger($output);
61
62
        $this->recalculate($logger);
63
    }
64
65
    /**
66
     * @param AbstractLogger $logger
67
     *
68
     * @return int
69
     */
70
    public function recalculate(AbstractLogger $logger)
71
    {
72
        $logger->notice('Recalculating contacting activities...');
73
        $logger->info(sprintf('<info>Processing started at %s</info>', date('Y-m-d H:i:s')));
74
75
        /** @var ConfigProvider $activityConfigProvider */
76
        $activityConfigProvider = $this->getContainer()->get('oro_entity_config.provider.activity');
77
78
        /** @var ActivityContactProvider $activityContactProvider */
79
        $activityContactProvider   = $this->getContainer()->get('orocrm_activity_contact.provider');
80
        $contactingActivityClasses = $activityContactProvider->getSupportedActivityClasses();
81
82
        $entityConfigsWithApplicableActivities = $activityConfigProvider->filter(
83
            function (ConfigInterface $entity) use ($contactingActivityClasses) {
84
                return
85
                    $entity->get('activities')
86
                    && array_intersect($contactingActivityClasses, $entity->get('activities'));
87
            }
88
        );
89
90
        if ($entityConfigsWithApplicableActivities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entityConfigsWithApplicableActivities of type Oro\Bundle\EntityConfigB...onfig\ConfigInterface[] 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...
91
            $logger->info(
92
                sprintf(
93
                    '<comment>Total found %d entities with enabled contacting activities</comment>',
94
                    count($entityConfigsWithApplicableActivities)
95
                )
96
            );
97
98
            $this->em                     = $this->getContainer()->get('doctrine')->getManager();
99
            $this->activityListRepository = $this->em->getRepository(ActivityList::ENTITY_NAME);
100
101
            /** @var ActivityListener $activityListener */
102
            $activityListener = $this->getContainer()->get('orocrm_activity_contact.listener.activity_listener');
103
104
            /** @var ActivityListFilterHelper $activityListHelper */
105
            $activityListHelper = $this->getContainer()->get('oro_activity_list.filter.helper');
106
107
            foreach ($entityConfigsWithApplicableActivities as $activityScopeConfig) {
108
                $entityClassName = $activityScopeConfig->getId()->getClassName();
109
                if (TargetExcludeList::isExcluded($entityClassName)) {
110
                    continue;
111
                }
112
                $offset          = 0;
113
                $startTimestamp  = time();
114
                $allRecordIds    = $this->getTargetIds($entityClassName);
115
                $this->resetRecordsWithoutActivities($entityClassName, $allRecordIds);
116
                while ($allRecords = $this->getRecordsToRecalculate($entityClassName, $allRecordIds, $offset)) {
117
                    $needsFlush = false;
118
                    foreach ($allRecords as $record) {
119
                        $this->resetRecordStatistic($record);
120
121
                        /** @var QueryBuilder $qb */
122
                        $qb = $this->activityListRepository->getBaseActivityListQueryBuilder(
123
                            $entityClassName,
124
                            $record->getId()
125
                        );
126
                        $activityListHelper->addFiltersToQuery(
127
                            $qb,
128
                            ['activityType' => ['value' => $contactingActivityClasses]]
129
                        );
130
131
                        /** @var ActivityList[] $activities */
132
                        $activities = $qb->getQuery()->getResult();
133
                        if ($activities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $activities of type Oro\Bundle\ActivityListB...e\Entity\ActivityList[] 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...
134
                            foreach ($activities as $activityListItem) {
135
                                /** @var object $activity */
136
                                $activity = $this->em->getRepository($activityListItem->getRelatedActivityClass())
137
                                    ->find($activityListItem->getRelatedActivityId());
138
139
                                $activityListener->onAddActivity(new ActivityEvent($activity, $record));
140
                            }
141
142
                            $this->em->persist($record);
143
                            $needsFlush = true;
144
                        }
145
                    }
146
147
                    if ($needsFlush) {
148
                        $this->em->flush();
149
                    }
150
151
                    $this->em->clear();
152
153
                    $offset += self::BATCH_SIZE;
154
                }
155
156
                $endTimestamp = time();
157
                $logger->info(
158
                    sprintf(
159
                        'Entity "%s", %d records processed (<comment>%d sec.</comment>).',
160
                        $entityClassName,
161
                        count($allRecordIds),
162
                        ($endTimestamp - $startTimestamp)
163
                    )
164
                );
165
            }
166
        }
167
168
        $logger->info(sprintf('<info>Processing finished at %s</info>', date('Y-m-d H:i:s')));
169
170
        return self::STATUS_SUCCESS;
171
    }
172
173
    /**
174
     * @param string $entityClassName
175
     * @param array $recordIdsWithActivities
176
     */
177
    protected function resetRecordsWithoutActivities($entityClassName, array $recordIdsWithActivities)
178
    {
179
        $offset = 0;
180
        while ($records = $this->getRecordsToReset($entityClassName, $recordIdsWithActivities, $offset)) {
181
            array_map([$this, 'resetRecordStatistic'], $records);
182
            $this->em->flush();
183
            $this->em->clear();
184
            $offset += self::BATCH_SIZE;
185
        }
186
    }
187
188
    /**
189
     * Resets entity statistics.
190
     *
191
     * @param object $entity
192
     */
193
    protected function resetRecordStatistic($entity)
194
    {
195
        /** @var PropertyAccessor $accessor */
196
        $accessor = PropertyAccess::createPropertyAccessor();
197
198
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT, 0);
199
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT_IN, 0);
200
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT_OUT, 0);
201
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE, null);
202
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE_IN, null);
203
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE_OUT, null);
204
    }
205
206
    /**
207
     * @param string  $entityClassName
208
     * @param array   $ids
209
     * @param integer $offset
210
     *
211
     * @return array
212
     */
213
    protected function getRecordsToRecalculate($entityClassName, $ids, $offset)
214
    {
215
        $entityRepository = $this->em->getRepository($entityClassName);
216
217
        return $entityRepository->findBy(['id' => $ids], ['id' => 'ASC'], self::BATCH_SIZE, $offset);
218
    }
219
220
    /**
221
     * @param string  $entityClassName
222
     * @param array   $excludedIds
223
     * @param integer $offset
224
     *
225
     * @return array
226
     */
227
    protected function getRecordsToReset($entityClassName, array $excludedIds, $offset)
228
    {
229
        $qb = $this->em->getRepository($entityClassName)->createQueryBuilder('e');
230
231
        if ($excludedIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $excludedIds of type array 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...
232
            $qb->andWhere($qb->expr()->notIn('e.id', $excludedIds));
233
        }
234
235
        return $qb->setMaxResults(static::BATCH_SIZE)
236
            ->setFirstResult($offset)
237
            ->getQuery()
238
            ->getResult();
239
    }
240
241
    /**
242
     * Returns entity ids of records that have associated contacting activities
243
     *
244
     * @param string $className Target entity class name
245
     *
246
     * @return array
247
     */
248
    protected function getTargetIds($className)
249
    {
250
        /** @var ActivityContactProvider $activityContactProvider */
251
        $activityContactProvider   = $this->getContainer()->get('orocrm_activity_contact.provider');
252
        $contactingActivityClasses = $activityContactProvider->getSupportedActivityClasses();
253
254
        // we need try/catch here to avoid crash on non existing entity relation
255
        try {
256
            $result = $this->activityListRepository->createQueryBuilder('list')
257
                ->select('r.id')
258
                ->distinct(true)
259
                ->join('list.' . $this->getAssociationName($className), 'r')
260
                ->where('list.relatedActivityClass in (:applicableClasses)')
261
                ->setParameter('applicableClasses', $contactingActivityClasses)
262
                ->getQuery()
263
                ->getScalarResult();
264
265
            $result = array_map('current', $result);
266
        } catch (\Exception $e) {
267
            $result = [];
268
        }
269
270
        return $result;
271
    }
272
273
    /**
274
     * Get Association name
275
     *
276
     * @param string $className
277
     *
278
     * @return string
279
     */
280
    protected function getAssociationName($className)
281
    {
282
        return ExtendHelper::buildAssociationName(
283
            $className,
284
            ActivityListEntityConfigDumperExtension::ASSOCIATION_KIND
285
        );
286
    }
287
}
288