OrmTotalsExtension   F
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17
Metric Value
wmc 59
lcom 1
cbo 17
dl 0
loc 416
rs 3.5483

14 Methods

Rating   Name   Duplication   Size   Complexity  
A isApplicable() 0 4 1
A visitDatasource() 0 4 1
A __construct() 0 11 1
A processConfigs() 0 15 3
A visitMetadata() 0 8 1
A getPriority() 0 5 1
B getGroupParts() 0 22 5
B getTotalData() 0 28 6
C getRootIds() 0 50 10
B getData() 0 40 6
D applyFrontendFormatting() 0 32 9
B mergeTotals() 0 30 4
B visitResult() 0 28 6
B addPageLimits() 0 30 5

How to fix   Complexity   

Complex Class

Complex classes like OrmTotalsExtension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OrmTotalsExtension, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Oro\Bundle\DataGridBundle\Extension\Totals;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\ORM\Query\Expr;
7
use Doctrine\ORM\QueryBuilder;
8
9
use Symfony\Component\Translation\TranslatorInterface;
10
11
use Oro\Component\PhpUtils\ArrayUtil;
12
13
use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration;
14
use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject;
15
use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject;
16
use Oro\Bundle\DataGridBundle\Datasource\DatasourceInterface;
17
use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource;
18
use Oro\Bundle\DataGridBundle\Exception\LogicException;
19
use Oro\Bundle\DataGridBundle\Extension\AbstractExtension;
20
use Oro\Bundle\DataGridBundle\Extension\Formatter\Property\PropertyInterface;
21
22
use Oro\Bundle\LocaleBundle\Formatter\DateTimeFormatter;
23
use Oro\Bundle\LocaleBundle\Formatter\NumberFormatter;
24
25
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
26
27
/**
28
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
29
 */
30
class OrmTotalsExtension extends AbstractExtension
31
{
32
    /** @var TranslatorInterface */
33
    protected $translator;
34
35
    /** @var QueryBuilder */
36
    protected $masterQB;
37
38
    /** @var NumberFormatter */
39
    protected $numberFormatter;
40
41
    /** @var DateTimeFormatter */
42
    protected $dateTimeFormatter;
43
44
    /** @var AclHelper */
45
    protected $aclHelper;
46
47
    /** @var array */
48
    protected $groupParts = [];
49
50
    /**
51
     * @param TranslatorInterface $translator
52
     * @param NumberFormatter     $numberFormatter
53
     * @param DateTimeFormatter   $dateTimeFormatter
54
     * @param AclHelper           $aclHelper
55
     */
56
    public function __construct(
57
        TranslatorInterface $translator,
58
        NumberFormatter $numberFormatter,
59
        DateTimeFormatter $dateTimeFormatter,
60
        AclHelper $aclHelper
61
    ) {
62
        $this->translator        = $translator;
63
        $this->numberFormatter   = $numberFormatter;
64
        $this->dateTimeFormatter = $dateTimeFormatter;
65
        $this->aclHelper         = $aclHelper;
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function isApplicable(DatagridConfiguration $config)
72
    {
73
        return $config->getDatasourceType() === OrmDatasource::TYPE;
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function processConfigs(DatagridConfiguration $config)
80
    {
81
        $totalRows = $this->validateConfiguration(
82
            new Configuration(),
83
            ['totals' => $config->offsetGetByPath(Configuration::TOTALS_PATH)]
84
        );
85
86
        if (!empty($totalRows)) {
87
            foreach ($totalRows as $rowName => $rowConfig) {
88
                $this->mergeTotals($totalRows, $rowName, $rowConfig, $config->getName());
89
            }
90
91
            $config->offsetSetByPath(Configuration::TOTALS_PATH, $totalRows);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function visitDatasource(DatagridConfiguration $config, DatasourceInterface $datasource)
99
    {
100
        $this->masterQB = clone $datasource->getQueryBuilder();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Oro\Bundle\DataGridBundl...rce\DatasourceInterface as the method getQueryBuilder() does only exist in the following implementations of said interface: Oro\Bundle\DataGridBundl...ource\Orm\OrmDatasource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function visitResult(DatagridConfiguration $config, ResultsObject $result)
107
    {
108
        $onlyOnePage  = $result->getTotalRecords() === count($result->getData());
109
110
        $totalData = [];
111
        $totals    = $config->offsetGetByPath(Configuration::TOTALS_PATH);
112
        if (null !== $totals && $result->getData()) {
113
            foreach ($totals as $rowName => $rowConfig) {
114
                if ($onlyOnePage && $rowConfig[Configuration::TOTALS_HIDE_IF_ONE_PAGE_KEY]) {
115
                    unset($totals[$rowName]);
116
                    continue;
117
                }
118
119
                $totalData[$rowName] = $this->getTotalData(
120
                    $rowConfig,
121
                    $this->getData(
122
                        $result,
123
                        $rowConfig['columns'],
124
                        $rowConfig[Configuration::TOTALS_PER_PAGE_ROW_KEY],
125
                        $config->isDatasourceSkipAclApply()
126
                    )
127
                );
128
            }
129
        }
130
        $result->offsetAddToArray('options', ['totals' => $totalData]);
131
132
        return $result;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function visitMetadata(DatagridConfiguration $config, MetadataObject $metaData)
139
    {
140
        $totals = $config->offsetGetByPath(Configuration::TOTALS_PATH);
141
        $metaData
142
            ->offsetAddToArray('initialState', ['totals' => $totals])
143
            ->offsetAddToArray('state', ['totals' => $totals])
144
            ->offsetAddToArray(MetadataObject::REQUIRED_MODULES_KEY, ['orodatagrid/js/totals-builder']);
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function getPriority()
151
    {
152
        // should visit after all extensions
153
        return -250;
154
    }
155
156
    /**
157
     * Get Group by part of master query as array
158
     *
159
     * @return array
160
     */
161
    protected function getGroupParts()
162
    {
163
        if (empty($this->groupParts)) {
164
            $groupParts   = [];
165
            $groupByParts = $this->masterQB->getDQLPart('groupBy');
166
            if (!empty($groupByParts)) {
167
                /** @var Expr\GroupBy $groupByPart */
168
                foreach ($groupByParts as $groupByPart) {
169
                    foreach ($groupByPart->getParts() as $part) {
170
                        $groupParts = array_merge(
171
                            $groupParts,
172
                            array_map('trim', explode(',', $part))
173
                        );
174
                    }
175
                }
176
            }
177
            $this->groupParts = $groupParts;
178
        }
179
180
        return $this->groupParts;
181
182
    }
183
184
    /**
185
     * Get total row frontend data
186
     *
187
     * @param array $rowConfig Total row config
188
     * @param array $data Db result data for current total row config
189
     * @return array Array with array of columns total values and labels
190
     */
191
    protected function getTotalData($rowConfig, $data)
192
    {
193
        if (empty($data)) {
194
            return [];
195
        }
196
197
        $columns = [];
198
        foreach ($rowConfig['columns'] as $field => $total) {
199
            $column = [];
200
            if (isset($data[$field])) {
201
                $totalValue = $data[$field];
202
                if (isset($total[Configuration::TOTALS_FORMATTER_KEY])) {
203
                    $totalValue = $this->applyFrontendFormatting(
204
                        $totalValue,
205
                        $total[Configuration::TOTALS_FORMATTER_KEY]
206
                    );
207
                }
208
                $column['total'] = $totalValue;
209
            }
210
            if (isset($total[Configuration::TOTALS_LABEL_KEY])) {
211
                $column[Configuration::TOTALS_LABEL_KEY] =
212
                    $this->translator->trans($total[Configuration::TOTALS_LABEL_KEY]);
213
            }
214
            $columns[$field] = $column;
215
        };
216
217
        return ['columns' => $columns];
218
    }
219
220
    /**
221
     * Get root entities config data
222
     *
223
     * @param QueryBuilder $query
224
     * @return array with root entities config
225
     */
226
    protected function getRootIds(QueryBuilder $query)
227
    {
228
        $groupParts = $this->getGroupParts();
229
        $rootIds    = [];
230
        if (empty($groupParts)) {
231
            $rootIdentifiers = $query->getEntityManager()
232
                ->getClassMetadata($query->getRootEntities()[0])->getIdentifier();
233
            $rootAlias       = $query->getRootAliases()[0];
234
            foreach ($rootIdentifiers as $field) {
235
                $rootIds[] = [
236
                    'fieldAlias'  => $field,
237
                    'alias'       => $field,
238
                    'entityAlias' => $rootAlias
239
                ];
240
            }
241
        } else {
242
            foreach ($groupParts as $groupPart) {
243
                if (strpos($groupPart, '.')) {
244
                    list($rootAlias, $rootIdentifierPart) = explode('.', $groupPart);
245
                    $rootIds[] = [
246
                        'fieldAlias'  => $rootIdentifierPart,
247
                        'entityAlias' => $rootAlias,
248
                        'alias'       => $rootIdentifierPart
249
                    ];
250
                } else {
251
                    $selectParts = $this->masterQB->getDQLPart('select');
252
                    /** @var Expr\Select $selectPart */
253
                    foreach ($selectParts as $selectPart) {
254
                        foreach ($selectPart->getParts() as $part) {
255
                            if (preg_match('/^(.*)\sas\s(.*)$/i', $part, $matches)) {
256
                                if (count($matches) === 3 && $groupPart === $matches[2]) {
257
                                    $rootIds[] = [
258
                                        'fieldAlias' => $matches[1],
259
                                        'alias'      => $matches[2]
260
                                    ];
261
                                }
262
                            } else {
263
                                $rootIds[] = [
264
                                    'fieldAlias' => $groupPart,
265
                                    'alias'      => $groupPart
266
                                ];
267
                            }
268
                        }
269
                    }
270
                }
271
            }
272
        }
273
274
        return $rootIds;
275
    }
276
277
    /**
278
     * Get total row data from database
279
     *
280
     * @param ResultsObject $pageData Grid page data
281
     * @param array $columnsConfig Total row columns config
282
     * @param bool $perPage Get data only for page data or for all data
283
     * @param bool $skipAclWalkerCheck Check Acl with acl helper or not
284
     * @return array
285
     */
286
    protected function getData(ResultsObject $pageData, $columnsConfig, $perPage = false, $skipAclWalkerCheck = false)
287
    {
288
        // todo: Need refactor this method. If query has not order by part and doesn't have id's in select, result
289
        //       can be unexpected
290
        $totalQueries = [];
291
        foreach ($columnsConfig as $field => $totalData) {
292
            if (isset($totalData[Configuration::TOTALS_SQL_EXPRESSION_KEY])
293
                && $totalData[Configuration::TOTALS_SQL_EXPRESSION_KEY]
294
            ) {
295
                $totalQueries[] = $totalData[Configuration::TOTALS_SQL_EXPRESSION_KEY] . ' AS ' . $field;
296
            }
297
        };
298
299
        $queryBuilder = clone $this->masterQB;
300
        $queryBuilder
301
            ->select($totalQueries)
302
            ->resetDQLPart('groupBy');
303
304
        $parameters = $queryBuilder->getParameters();
305
        if ($parameters->count()) {
306
            $queryBuilder->resetDQLPart('where')
307
                ->resetDQLPart('having')
308
                ->setParameters(new ArrayCollection());
309
        }
310
311
        $this->addPageLimits($queryBuilder, $pageData, $perPage);
312
313
        $query = $queryBuilder->getQuery();
314
315
        if (!$skipAclWalkerCheck) {
316
            $query = $this->aclHelper->apply($query);
317
        }
318
319
        $resultData = $query
320
            ->setFirstResult(null)
321
            ->setMaxResults(1)
322
            ->getScalarResult();
323
324
        return array_shift($resultData);
325
    }
326
327
    /**
328
     * Add "in" expression as page limit to query builder
329
     *
330
     * @param QueryBuilder $dataQueryBuilder
331
     * @param ResultsObject $pageData
332
     * @param bool $perPage
333
     */
334
    protected function addPageLimits(QueryBuilder $dataQueryBuilder, ResultsObject $pageData, $perPage)
335
    {
336
        $rootIdentifiers = $this->getRootIds($dataQueryBuilder);
337
338
        if (!$perPage) {
339
            $queryBuilder = clone $this->masterQB;
340
            $data = $queryBuilder
341
                ->getQuery()
342
                ->setFirstResult(null)
343
                ->setMaxResults(null)
344
                ->getScalarResult();
345
        } else {
346
            $data = $pageData->getData();
347
        }
348
        foreach ($rootIdentifiers as $identifier) {
349
            $ids = ArrayUtil::arrayColumn($data, $identifier['alias']);
350
351
            $field = isset($identifier['entityAlias'])
352
                ? $identifier['entityAlias'] . '.' . $identifier['fieldAlias']
353
                : $identifier['fieldAlias'];
354
355
            $filteredIds = array_filter($ids);
356
            if (empty($filteredIds)) {
357
                continue;
358
            }
359
360
            $dataQueryBuilder->andWhere($dataQueryBuilder->expr()->in($field, $ids));
361
        }
362
363
    }
364
365
    /**
366
     * Apply formatting to totals values
367
     *
368
     * @param mixed|null $val
369
     * @param string|null $formatter
370
     * @return string|null
371
     */
372
    protected function applyFrontendFormatting($val = null, $formatter = null)
373
    {
374
        if (null === $formatter) {
375
            return $val;
376
        }
377
378
        switch ($formatter) {
379
            case PropertyInterface::TYPE_DATE:
380
                $val = $this->dateTimeFormatter->formatDate($val);
381
                break;
382
            case PropertyInterface::TYPE_DATETIME:
383
                $val = $this->dateTimeFormatter->format($val);
384
                break;
385
            case PropertyInterface::TYPE_TIME:
386
                $val = $this->dateTimeFormatter->formatTime($val);
387
                break;
388
            case PropertyInterface::TYPE_DECIMAL:
389
                $val = $this->numberFormatter->formatDecimal($val);
390
                break;
391
            case PropertyInterface::TYPE_INTEGER:
392
                $val = $this->numberFormatter->formatDecimal($val);
393
                break;
394
            case PropertyInterface::TYPE_PERCENT:
395
                $val = $this->numberFormatter->formatPercent($val);
396
                break;
397
            case PropertyInterface::TYPE_CURRENCY:
398
                $val = $this->numberFormatter->formatCurrency($val);
399
                break;
400
        }
401
402
        return $val;
403
    }
404
405
    /**
406
     * Merge total rows configs
407
     *
408
     * @param array $totalRows
409
     * @param string $rowName
410
     * @param array $rowConfig
411
     * @param string $gridName
412
     * @return array
413
     * @throws LogicException
414
     */
415
    protected function mergeTotals(&$totalRows, $rowName, $rowConfig, $gridName)
416
    {
417
        if (isset($rowConfig[Configuration::TOTALS_EXTEND_KEY]) && $rowConfig[Configuration::TOTALS_EXTEND_KEY]) {
418
            if (!isset($totalRows[$rowConfig[Configuration::TOTALS_EXTEND_KEY]])) {
419
                throw new LogicException(sprintf(
420
                    'Total row "%s" definition in "%s" datagrid config does not exist',
421
                    $rowConfig[Configuration::TOTALS_EXTEND_KEY],
422
                    $gridName
423
                ));
424
            }
425
426
            $parentConfig = $this->mergeTotals(
427
                $totalRows,
428
                $rowConfig[Configuration::TOTALS_EXTEND_KEY],
429
                $totalRows[$rowConfig[Configuration::TOTALS_EXTEND_KEY]],
430
                $gridName
431
            );
432
433
            $rowConfig = array_replace_recursive(
434
                $parentConfig,
435
                $totalRows[$rowName]
436
            );
437
            unset($totalRows[$rowName][Configuration::TOTALS_EXTEND_KEY]);
438
439
            $totalRows[$rowName] = $rowConfig;
440
441
        }
442
443
        return $rowConfig;
444
    }
445
}
446