Completed
Pull Request — master (#2743)
by Jeroen
14:54
created

PagePartAdmin   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 383
Duplicated Lines 3.92 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 12
dl 15
loc 383
c 0
b 0
f 0
ccs 0
cts 205
cp 0
rs 6.4799

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 4
B initializePageParts() 4 36 7
A getPage() 0 4 1
F preBindRequest() 0 73 16
A bindRequest() 0 3 1
A adaptForm() 0 16 3
B persist() 0 35 7
A getContext() 0 4 1
A getPossiblePagePartTypes() 0 29 5
A getName() 0 4 1
A getPagePartMap() 0 4 1
A getType() 0 11 3
A getPagePart() 0 7 1
A getClassName() 0 4 1
A dispatch() 11 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PagePartAdmin 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 PagePartAdmin, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kunstmaan\PagePartBundle\PagePartAdmin;
4
5
use Doctrine\ORM\EntityManager;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Kunstmaan\AdminBundle\Entity\EntityInterface;
8
use Kunstmaan\PagePartBundle\Entity\PagePartRef;
9
use Kunstmaan\PagePartBundle\Event\Events;
10
use Kunstmaan\PagePartBundle\Event\PagePartEvent;
11
use Kunstmaan\PagePartBundle\Helper\HasPagePartsInterface;
12
use Kunstmaan\PagePartBundle\Helper\PagePartInterface;
13
use Kunstmaan\PagePartBundle\Repository\PagePartRefRepository;
14
use Kunstmaan\UtilitiesBundle\Helper\ClassLookup;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
17
use Symfony\Component\Form\FormBuilderInterface;
18
use Symfony\Component\HttpFoundation\Request;
19
20
/**
21
 * PagePartAdmin
22
 */
23
class PagePartAdmin
24
{
25
    /**
26
     * @var PagePartAdminConfiguratorInterface
27
     */
28
    protected $configurator;
29
30
    /**
31
     * @var EntityManager|EntityManagerInterface
32
     */
33
    protected $em;
34
35
    /**
36
     * @var HasPagePartsInterface
37
     */
38
    protected $page;
39
40
    /**
41
     * @var string
42
     */
43
    protected $context;
44
45
    /**
46
     * @var ContainerInterface
47
     */
48
    protected $container;
49
50
    /**
51
     * @var PagePartInterface[]
52
     */
53
    protected $pageParts = array();
54
55
    /**
56
     * @var PagePartRef[]
57
     */
58
    protected $pagePartRefs = array();
59
60
    /**
61
     * @var PagePartInterface[]
62
     */
63
    protected $newPageParts = array();
64
65
    /**
66
     * @param PagePartAdminConfiguratorInterface $configurator The configurator
67
     * @param EntityManagerInterface             $em           The entity manager
68
     * @param HasPagePartsInterface              $page         The page
69
     * @param string|null                        $context      The context
70
     * @param ContainerInterface|null            $container    The container
71
     *
72
     * @throws \InvalidArgumentException
73
     */
74
    public function __construct(PagePartAdminConfiguratorInterface $configurator, EntityManagerInterface $em, HasPagePartsInterface $page, $context = null, ContainerInterface $container = null)
75
    {
76
        if (!($page instanceof EntityInterface)) {
77
            throw new \InvalidArgumentException('Page must be an instance of EntityInterface.');
78
        }
79
80
        $this->configurator = $configurator;
81
        $this->em = $em;
82
        $this->page = $page;
83
        $this->container = $container;
84
85
        if ($context) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
86
            $this->context = $context;
87
        } elseif ($this->configurator->getContext()) {
88
            $this->context = $this->configurator->getContext();
89
        } else {
90
            $this->context = 'main';
91
        }
92
93
        $this->initializePageParts();
94
    }
95
96
    /**
97
     * Get all pageparts from the database, and store them.
98
     */
99
    private function initializePageParts()
100
    {
101
        // Get all the pagepartrefs
102
        /** @var PagePartRefRepository $ppRefRepo */
103
        $ppRefRepo = $this->em->getRepository(PagePartRef::class);
104
        $ppRefs = $ppRefRepo->getPagePartRefs($this->page, $this->context);
105
106
        // Group pagepartrefs per type
107
        $types = array();
108
        foreach ($ppRefs as $pagePartRef) {
109
            $types[$pagePartRef->getPagePartEntityname()][] = $pagePartRef->getPagePartId();
110
            $this->pagePartRefs[$pagePartRef->getId()] = $pagePartRef;
111
        }
112
113
        // Fetch all the pageparts (only one query per pagepart type)
114
        /** @var EntityInterface[] $pageParts */
115
        $pageParts = array();
116 View Code Duplication
        foreach ($types as $classname => $ids) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
117
            $result = $this->em->getRepository($classname)->findBy(array('id' => $ids));
118
            $pageParts = array_merge($pageParts, $result);
119
        }
120
121
        // Link the pagepartref to the pagepart
122
        foreach ($this->pagePartRefs as $pagePartRef) {
123
            foreach ($pageParts as $key => $pagePart) {
124
                if (ClassLookup::getClass($pagePart) == $pagePartRef->getPagePartEntityname()
125
                    && $pagePart->getId() == $pagePartRef->getPagePartId()
126
                ) {
127
                    $this->pageParts[$pagePartRef->getId()] = $pagePart;
128
                    unset($pageParts[$key]);
129
130
                    break;
131
                }
132
            }
133
        }
134
    }
135
136
    /**
137
     * @return EntityInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be HasPagePartsInterface?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
138
     */
139
    public function getPage()
140
    {
141
        return $this->page;
142
    }
143
144
    /**
145
     * @param Request $request
146
     */
147
    public function preBindRequest(Request $request)
148
    {
149
        // Fetch all sub-entities that should be removed
150
        $subPagePartsToDelete = array();
151
        foreach (array_keys($request->request->all()) as $key) {
152
            // Example value: delete_pagepartadmin_74_tags_3
153
            if (preg_match('/^delete_pagepartadmin_(\\d+)_(\\w+)_(\\d+)$/i', $key, $matches)) {
154
                $subPagePartsToDelete[$matches[1]][] = array('name' => $matches[2], 'id' => $matches[3]);
155
            }
156
        }
157
158
        $doFlush = false;
159
        foreach ($this->pagePartRefs as $pagePartRef) {
160
            // Remove pageparts
161
            if ('true' == $request->get($pagePartRef->getId().'_deleted')) {
162
                $pagePart = $this->pageParts[$pagePartRef->getId()];
163
                $this->em->remove($pagePart);
164
                $this->em->remove($pagePartRef);
165
166
                unset($this->pageParts[$pagePartRef->getId()], $this->pagePartRefs[$pagePartRef->getId()]);
167
                $doFlush = true;
168
            }
169
170
            // Remove sub-entities from pageparts
171
            if (\array_key_exists($pagePartRef->getId(), $subPagePartsToDelete)) {
172
                $pagePart = $this->pageParts[$pagePartRef->getId()];
173
                foreach ($subPagePartsToDelete[$pagePartRef->getId()] as $deleteInfo) {
174
                    /** @var EntityInterface[] $objects */
175
                    $objects = \call_user_func(array($pagePart, 'get'.ucfirst($deleteInfo['name'])));
176
177
                    foreach ($objects as $object) {
178
                        if ($object->getId() == $deleteInfo['id']) {
179
                            $this->em->remove($object);
180
                            $doFlush = true;
181
                        }
182
                    }
183
                }
184
            }
185
        }
186
187
        if ($doFlush) {
188
            $this->em->flush();
189
        }
190
191
        // Create the objects for the new pageparts
192
        $this->newPageParts = array();
193
        $newRefIds = $request->get($this->context.'_new');
194
195
        if (\is_array($newRefIds)) {
196
            foreach ($newRefIds as $newId) {
197
                $type = $request->get($this->context.'_type_'.$newId);
198
                $this->newPageParts[$newId] = new $type();
199
            }
200
        }
201
202
        // Sort pageparts again
203
        $sequences = $request->get($this->context.'_sequence');
204
        if (!\is_null($sequences)) {
205
            $tempPageparts = $this->pageParts;
206
            $this->pageParts = array();
207
            foreach ($sequences as $sequence) {
0 ignored issues
show
Bug introduced by
The expression $sequences of type object|integer|double|string|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
208
                if (\array_key_exists($sequence, $this->newPageParts)) {
209
                    $this->pageParts[$sequence] = $this->newPageParts[$sequence];
210
                } elseif (\array_key_exists($sequence, $tempPageparts)) {
211
                    $this->pageParts[$sequence] = $tempPageparts[$sequence];
212
                } else {
213
                    $this->pageParts[$sequence] = $this->getPagePart($sequence, array_search($sequence, $sequences) + 1);
214
                }
215
            }
216
217
            unset($tempPageparts);
218
        }
219
    }
220
221
    /**
222
     * @param Request $request
223
     */
224
    public function bindRequest(Request $request)
225
    {
226
    }
227
228
    /**
229
     * @param FormBuilderInterface $formbuilder
230
     */
231
    public function adaptForm(FormBuilderInterface $formbuilder)
232
    {
233
        $data = $formbuilder->getData();
234
235
        foreach ($this->pageParts as $pagePartRefId => $pagePart) {
236
            $data['pagepartadmin_' . $pagePartRefId] = $pagePart;
237
            $formbuilder->add('pagepartadmin_' . $pagePartRefId, $pagePart->getDefaultAdminType());
238
        }
239
240
        foreach ($this->newPageParts as $newPagePartRefId => $newPagePart) {
241
            $data['pagepartadmin_' . $newPagePartRefId] = $newPagePart;
242
            $formbuilder->add('pagepartadmin_' . $newPagePartRefId, $newPagePart->getDefaultAdminType());
243
        }
244
245
        $formbuilder->setData($data);
246
    }
247
248
    /**
249
     * @param Request $request
250
     */
251
    public function persist(Request $request)
252
    {
253
        /** @var PagePartRefRepository $ppRefRepo */
254
        $ppRefRepo = $this->em->getRepository(PagePartRef::class);
255
256
        // Add new pageparts on the correct position + Re-order and save pageparts if needed
257
        $sequences = $request->get($this->context.'_sequence', []);
258
        $sequencescount = \count($sequences);
259
        for ($i = 0; $i < $sequencescount; ++$i) {
260
            $pagePartRefId = $sequences[$i];
261
262
            if (\array_key_exists($pagePartRefId, $this->newPageParts)) {
263
                $pagePart = $this->newPageParts[$pagePartRefId];
264
                $this->em->persist($pagePart);
265
                $this->em->flush($pagePart);
0 ignored issues
show
Unused Code introduced by
The call to EntityManagerInterface::flush() has too many arguments starting with $pagePart.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
266
267
                $ppRefRepo->addPagePart($this->page, $pagePart, $i + 1, $this->context, false);
268
            } elseif (\array_key_exists($pagePartRefId, $this->pagePartRefs)) {
269
                $pagePartRef = $this->pagePartRefs[$pagePartRefId];
270
                if ($pagePartRef instanceof PagePartRef && $pagePartRef->getSequencenumber() != ($i + 1)) {
271
                    $pagePartRef->setSequencenumber($i + 1);
272
                    $pagePartRef->setContext($this->context);
273
                    $this->em->persist($pagePartRef);
274
                }
275
                $pagePart = $pagePartRef->getPagePart($this->em);
0 ignored issues
show
Bug introduced by
It seems like $this->em can also be of type object<Doctrine\ORM\EntityManagerInterface>; however, Kunstmaan\PagePartBundle...ePartRef::getPagePart() does only seem to accept object<Doctrine\ORM\EntityManager>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
276
            }
277
278
            if (isset($pagePart)) {
279
                $this->dispatch(
280
                    new PagePartEvent($pagePart),
281
                    Events::POST_PERSIST
282
                );
283
            }
284
        }
285
    }
286
287
    /**
288
     * @return string|null
289
     */
290
    public function getContext()
291
    {
292
        return $this->context;
293
    }
294
295
    /**
296
     * This getter returns an array holding info on page part types that can be added to the page.
297
     * The types are filtererd here, based on the amount of page parts of a certain type that can be added to the page.
298
     *
299
     * @return array
300
     */
301
    public function getPossiblePagePartTypes()
302
    {
303
        $possiblePPTypes = $this->configurator->getPossiblePagePartTypes();
304
        $result = array();
305
306
        // filter page part types that can only be added x times to the page context.
307
        // to achieve this, provide a 'pagelimit' parameter when adding the pp type in your PagePartAdminConfiguration
308
        if (!empty($possiblePPTypes)) {
309
            foreach ($possiblePPTypes as $possibleTypeData) {
310
                if (\array_key_exists('pagelimit', $possibleTypeData)) {
311
                    $pageLimit = $possibleTypeData['pagelimit'];
312
                    /** @var PagePartRefRepository $entityRepository */
313
                    $entityRepository = $this->em->getRepository(PagePartRef::class);
314
                    $formPPCount = $entityRepository->countPagePartsOfType(
315
                        $this->page,
316
                        $possibleTypeData['class'],
317
                        $this->configurator->getContext()
318
                    );
319
                    if ($formPPCount < $pageLimit) {
320
                        $result[] = $possibleTypeData;
321
                    }
322
                } else {
323
                    $result[] = $possibleTypeData;
324
                }
325
            }
326
        }
327
328
        return $result;
329
    }
330
331
    /**
332
     * @return string
333
     */
334
    public function getName()
335
    {
336
        return $this->configurator->getName();
337
    }
338
339
    /**
340
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use PagePartInterface[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
341
     */
342
    public function getPagePartMap()
343
    {
344
        return $this->pageParts;
345
    }
346
347
    /**
348
     * @param PagePartInterface $pagepart
349
     *
350
     * @return string
351
     */
352
    public function getType(PagePartInterface $pagepart)
353
    {
354
        $possiblePagePartTypes = $this->configurator->getPossiblePagePartTypes();
355
        foreach ($possiblePagePartTypes as &$pageparttype) {
356
            if ($pageparttype['class'] == ClassLookup::getClass($pagepart)) {
357
                return $pageparttype['name'];
358
            }
359
        }
360
361
        return 'no name';
362
    }
363
364
    /**
365
     * @param int $id
366
     * @param int $sequenceNumber
367
     *
368
     * @return PagePartInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be PagePartInterface|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
369
     */
370
    public function getPagePart($id, $sequenceNumber)
371
    {
372
        /** @var PagePartRefRepository $ppRefRepo */
373
        $ppRefRepo = $this->em->getRepository(PagePartRef::class);
374
375
        return $ppRefRepo->getPagePart($id, $this->context, $sequenceNumber);
376
    }
377
378
    /**
379
     * @param object $pagepart
380
     *
381
     * @return string
382
     */
383
    public function getClassName($pagepart)
384
    {
385
        return \get_class($pagepart);
386
    }
387
388
    /**
389
     * @param object $event
390
     * @param string $eventName
391
     *
392
     * @return object
393
     */
394 View Code Duplication
    private function dispatch($event, string $eventName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
395
    {
396
        $eventDispatcher = $this->container->get('event_dispatcher');
397
        if (class_exists(LegacyEventDispatcherProxy::class)) {
398
            $eventDispatcher = LegacyEventDispatcherProxy::decorate($eventDispatcher);
399
400
            return $eventDispatcher->dispatch($event, $eventName);
401
        }
402
403
        return $eventDispatcher->dispatch($eventName, $event);
404
    }
405
}
406