Completed
Push — 1.9 ( 30c3e5...ca8b96 )
by
unknown
42:25
created

ActivityContactRecalculateCommand::execute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
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->info('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
            $this->em                     = $this->getContainer()->get('doctrine')->getManager();
98
            $this->activityListRepository = $this->em->getRepository(ActivityList::ENTITY_NAME);
99
100
            /** @var ActivityListener $activityListener */
101
            $activityListener = $this->getContainer()->get('orocrm_activity_contact.listener.activity_listener');
102
            /** @var ActivityListFilterHelper $activityListHelper */
103
            $activityListHelper = $this->getContainer()->get('oro_activity_list.filter.helper');
104
105
            foreach ($entityConfigsWithApplicableActivities as $activityScopeConfig) {
106
                $entityClassName = $activityScopeConfig->getId()->getClassName();
107
                if (TargetExcludeList::isExcluded($entityClassName)) {
108
                    continue;
109
                }
110
                $offset          = 0;
111
                $startTimestamp  = time();
112
                $allRecordIds    = $this->getTargetIds($entityClassName);
113
                $this->resetRecordsWithoutActivities($entityClassName, $allRecordIds);
114
                while ($allRecords = $this->getRecordsToRecalculate($entityClassName, $allRecordIds, $offset)) {
115
                    $needsFlush = false;
116
                    foreach ($allRecords as $record) {
117
                        $this->resetRecordStatistic($record);
118
                        /** @var QueryBuilder $qb */
119
                        $qb = $this->activityListRepository->getBaseActivityListQueryBuilder(
120
                            $entityClassName,
121
                            $record->getId()
122
                        );
123
                        $activityListHelper->addFiltersToQuery(
124
                            $qb,
125
                            ['activityType' => ['value' => $contactingActivityClasses]]
126
                        );
127
128
                        /** @var ActivityList[] $activities */
129
                        $activities = $qb->getQuery()->getResult();
130
                        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...
131
                            foreach ($activities as $activityListItem) {
132
                                /** @var object $activity */
133
                                $activity = $this->em->getRepository($activityListItem->getRelatedActivityClass())
134
                                    ->find($activityListItem->getRelatedActivityId());
135
136
                                $activityListener->onAddActivity(new ActivityEvent($activity, $record));
137
                            }
138
                            $this->em->persist($record);
139
                            $needsFlush = true;
140
                        }
141
                    }
142
                    if ($needsFlush) {
143
                        $this->em->flush();
144
                    }
145
                    $this->em->clear();
146
                    $offset += self::BATCH_SIZE;
147
                }
148
149
                $endTimestamp = time();
150
                $logger->info(
151
                    sprintf(
152
                        'Entity "%s", %d records processed (<comment>%d sec.</comment>).',
153
                        $entityClassName,
154
                        count($allRecordIds),
155
                        ($endTimestamp - $startTimestamp)
156
                    )
157
                );
158
            }
159
        }
160
        $logger->info(sprintf('<info>Processing finished at %s</info>', date('Y-m-d H:i:s')));
161
162
        return self::STATUS_SUCCESS;
163
    }
164
165
    /**
166
     * @param string $entityClassName
167
     * @param array $recordIdsWithActivities
168
     */
169
    protected function resetRecordsWithoutActivities($entityClassName, array $recordIdsWithActivities)
170
    {
171
        $offset = 0;
172
        while ($records = $this->getRecordsToReset($entityClassName, $recordIdsWithActivities, $offset)) {
173
            array_map([$this, 'resetRecordStatistic'], $records);
174
            $this->em->flush();
175
            $this->em->clear();
176
            $offset += self::BATCH_SIZE;
177
        }
178
    }
179
180
    /**
181
     * Resets entity statistics.
182
     *
183
     * @param object $entity
184
     */
185
    protected function resetRecordStatistic($entity)
186
    {
187
        /** @var PropertyAccessor $accessor */
188
        $accessor = PropertyAccess::createPropertyAccessor();
189
190
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT, 0);
191
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT_IN, 0);
192
        $accessor->setValue($entity, ActivityScope::CONTACT_COUNT_OUT, 0);
193
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE, null);
194
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE_IN, null);
195
        $accessor->setValue($entity, ActivityScope::LAST_CONTACT_DATE_OUT, null);
196
    }
197
198
    /**
199
     * @param string  $entityClassName
200
     * @param array   $ids
201
     * @param integer $offset
202
     *
203
     * @return array
204
     */
205
    protected function getRecordsToRecalculate($entityClassName, $ids, $offset)
206
    {
207
        $entityRepository = $this->em->getRepository($entityClassName);
208
209
        return $entityRepository->findBy(['id' => $ids], ['id' => 'ASC'], self::BATCH_SIZE, $offset);
210
    }
211
212
    /**
213
     * @param string  $entityClassName
214
     * @param array   $excludedIds
215
     * @param integer $offset
216
     *
217
     * @return array
218
     */
219
    protected function getRecordsToReset($entityClassName, array $excludedIds, $offset)
220
    {
221
        $qb = $this->em->getRepository($entityClassName)->createQueryBuilder('e');
222
223
        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...
224
            $qb->andWhere($qb->expr()->notIn('e.id', $excludedIds));
225
        }
226
227
        return $qb->setMaxResults(static::BATCH_SIZE)
228
            ->setFirstResult($offset)
229
            ->getQuery()
230
            ->getResult();
231
    }
232
233
    /**
234
     * Returns entity ids of records that have associated contacting activities
235
     *
236
     * @param string $className Target entity class name
237
     *
238
     * @return array
239
     */
240
    protected function getTargetIds($className)
241
    {
242
        /** @var ActivityContactProvider $activityContactProvider */
243
        $activityContactProvider   = $this->getContainer()->get('orocrm_activity_contact.provider');
244
        $contactingActivityClasses = $activityContactProvider->getSupportedActivityClasses();
245
246
        // we need try/catch here to avoid crash on non existing entity relation
247
        try {
248
            $result = $this->activityListRepository->createQueryBuilder('list')
249
                ->select('r.id')
250
                ->distinct(true)
251
                ->join('list.' . $this->getAssociationName($className), 'r')
252
                ->where('list.relatedActivityClass in (:applicableClasses)')
253
                ->setParameter('applicableClasses', $contactingActivityClasses)
254
                ->getQuery()
255
                ->getScalarResult();
256
257
            $result = array_map('current', $result);
258
        } catch (\Exception $e) {
259
            $result = [];
260
        }
261
262
        return $result;
263
    }
264
265
    /**
266
     * Get Association name
267
     *
268
     * @param string $className
269
     *
270
     * @return string
271
     */
272
    protected function getAssociationName($className)
273
    {
274
        return ExtendHelper::buildAssociationName(
275
            $className,
276
            ActivityListEntityConfigDumperExtension::ASSOCIATION_KIND
277
        );
278
    }
279
}
280