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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.