ActivityContactRecalculateCommand   C
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 23

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 23
dl 0
loc 250
rs 5.5
c 0
b 0
f 0

9 Methods

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