Passed
Push — master ( 2e1806...f0947f )
by Jan
07:30
created

BaseAdminController   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 219
c 0
b 0
f 0
dl 0
loc 418
rs 3.6
wmc 60

10 Methods

Rating   Name   Duplication   Size   Complexity  
A _exportEntity() 0 5 1
A _exportAll() 0 7 1
A revertElementIfNeeded() 0 17 3
A additionalActionEdit() 0 3 1
C _edit() 0 104 14
A additionalActionNew() 0 3 1
A deleteCheck() 0 13 3
B _delete() 0 52 9
C _new() 0 122 17
B __construct() 0 29 10

How to fix   Complexity   

Complex Class

Complex classes like BaseAdminController 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.

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 BaseAdminController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Controller\AdminPages;
24
25
use App\DataTables\LogDataTable;
26
use App\Entity\Attachments\Attachment;
27
use App\Entity\Base\AbstractDBElement;
28
use App\Entity\Base\AbstractNamedDBElement;
29
use App\Entity\Base\AbstractPartsContainingDBElement;
30
use App\Entity\Base\AbstractStructuralDBElement;
31
use App\Entity\Base\PartsContainingRepositoryInterface;
32
use App\Entity\LabelSystem\LabelProfile;
33
use App\Entity\Parameters\AbstractParameter;
34
use App\Entity\UserSystem\User;
35
use App\Exceptions\AttachmentDownloadException;
36
use App\Form\AdminPages\ImportType;
37
use App\Form\AdminPages\MassCreationForm;
38
use App\Repository\AbstractPartsContainingRepository;
39
use App\Services\Attachments\AttachmentSubmitHandler;
40
use App\Services\ImportExportSystem\EntityExporter;
41
use App\Services\ImportExportSystem\EntityImporter;
42
use App\Services\LabelSystem\Barcodes\BarcodeExampleElementsGenerator;
43
use App\Services\LabelSystem\LabelGenerator;
44
use App\Services\LogSystem\EventCommentHelper;
45
use App\Services\LogSystem\HistoryHelper;
46
use App\Services\LogSystem\TimeTravel;
47
use App\Services\Trees\StructuralElementRecursionHelper;
48
use DateTime;
49
use Doctrine\ORM\EntityManagerInterface;
50
use InvalidArgumentException;
51
use Omines\DataTablesBundle\DataTableFactory;
52
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
53
use Symfony\Component\EventDispatcher\EventDispatcher;
54
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
55
use Symfony\Component\Form\FormInterface;
56
use Symfony\Component\HttpFoundation\File\UploadedFile;
57
use Symfony\Component\HttpFoundation\RedirectResponse;
58
use Symfony\Component\HttpFoundation\Request;
59
use Symfony\Component\HttpFoundation\Response;
60
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
61
use Symfony\Component\Validator\ConstraintViolationList;
62
use Symfony\Contracts\Translation\TranslatorInterface;
63
64
use function Symfony\Component\Translation\t;
65
66
abstract class BaseAdminController extends AbstractController
67
{
68
    protected string $entity_class = '';
69
    protected string $form_class = '';
70
    protected string $twig_template = '';
71
    protected string $route_base = '';
72
    protected string $attachment_class = '';
73
    protected ?string $parameter_class = '';
74
75
    protected UserPasswordHasherInterface $passwordEncoder;
76
    protected TranslatorInterface $translator;
77
    protected AttachmentSubmitHandler $attachmentSubmitHandler;
78
    protected EventCommentHelper $commentHelper;
79
80
    protected HistoryHelper $historyHelper;
81
    protected TimeTravel $timeTravel;
82
    protected DataTableFactory $dataTableFactory;
83
    /**
84
     * @var EventDispatcher|EventDispatcherInterface
85
     */
86
    protected $eventDispatcher;
87
    protected LabelGenerator $labelGenerator;
88
    protected BarcodeExampleElementsGenerator $barcodeExampleGenerator;
89
90
    protected EntityManagerInterface $entityManager;
91
92
    public function __construct(TranslatorInterface $translator, UserPasswordHasherInterface $passwordEncoder,
93
        AttachmentSubmitHandler $attachmentSubmitHandler,
94
        EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
95
        DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, BarcodeExampleElementsGenerator $barcodeExampleGenerator,
96
        LabelGenerator $labelGenerator, EntityManagerInterface $entityManager)
97
    {
98
        if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
99
            throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
100
        }
101
102
        if ('' === $this->attachment_class || !is_a($this->attachment_class, Attachment::class, true)) {
103
            throw new InvalidArgumentException('You have to override the $attachment_class value with a valid Attachment class in your subclass!');
104
        }
105
106
        if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) {
107
            throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!');
108
        }
109
110
        $this->translator = $translator;
111
        $this->passwordEncoder = $passwordEncoder;
112
        $this->attachmentSubmitHandler = $attachmentSubmitHandler;
113
        $this->commentHelper = $commentHelper;
114
        $this->historyHelper = $historyHelper;
115
        $this->timeTravel = $timeTravel;
116
        $this->dataTableFactory = $dataTableFactory;
117
        $this->eventDispatcher = $eventDispatcher;
118
        $this->barcodeExampleGenerator = $barcodeExampleGenerator;
119
        $this->labelGenerator = $labelGenerator;
120
        $this->entityManager = $entityManager;
121
    }
122
123
    protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime
124
    {
125
        if (null !== $timestamp) {
126
            $this->denyAccessUnlessGranted('show_history', $entity);
127
            //If the timestamp only contains numbers interpret it as unix timestamp
128
            if (ctype_digit($timestamp)) {
129
                $timeTravel_timestamp = new DateTime();
130
                $timeTravel_timestamp->setTimestamp((int) $timestamp);
131
            } else { //Try to parse it via DateTime
132
                $timeTravel_timestamp = new DateTime($timestamp);
133
            }
134
            $this->timeTravel->revertEntityToTimestamp($entity, $timeTravel_timestamp);
135
136
            return $timeTravel_timestamp;
137
        }
138
139
        return null;
140
    }
141
142
    /**
143
     * Perform some additional actions, when the form was valid, but before the entity is saved.
144
     *
145
     * @return bool return true, to save entity normally, return false, to abort saving
146
     */
147
    protected function additionalActionEdit(FormInterface $form, AbstractNamedDBElement $entity): bool
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

147
    protected function additionalActionEdit(/** @scrutinizer ignore-unused */ FormInterface $form, AbstractNamedDBElement $entity): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
148
    {
149
        return true;
150
    }
151
152
    protected function _edit(AbstractNamedDBElement $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
153
    {
154
        $this->denyAccessUnlessGranted('read', $entity);
155
156
        $timeTravel_timestamp = $this->revertElementIfNeeded($entity, $timestamp);
157
158
        if ($this->isGranted('show_history', $entity)) {
159
            $table = $this->dataTableFactory->createFromType(
160
                LogDataTable::class,
161
                [
162
                    'filter_elements' => $this->historyHelper->getAssociatedElements($entity),
163
                    'mode' => 'element_history',
164
                ],
165
                ['pageLength' => 10]
166
            )
167
                ->handleRequest($request);
168
169
            if ($table->isCallback()) {
170
                return $table->getResponse();
171
            }
172
        } else {
173
            $table = null;
174
        }
175
176
        $form_options = [
177
            'attachment_class' => $this->attachment_class,
178
            'parameter_class' => $this->parameter_class,
179
            'disabled' => null !== $timeTravel_timestamp,
180
        ];
181
182
        //Disable editing of options, if user is not allowed to use twig...
183
        if (
184
            $entity instanceof LabelProfile
185
            && 'twig' === $entity->getOptions()->getLinesMode()
186
            && !$this->isGranted('@labels.use_twig')
187
        ) {
188
            $form_options['disable_options'] = true;
189
        }
190
191
        $form = $this->createForm($this->form_class, $entity, $form_options);
192
193
        $form->handleRequest($request);
194
        if ($form->isSubmitted() && $form->isValid()) {
195
            if ($this->additionalActionEdit($form, $entity)) {
196
                //Upload passed files
197
                $attachments = $form['attachments'];
198
                foreach ($attachments as $attachment) {
199
                    /** @var FormInterface $attachment */
200
                    $options = [
201
                        'secure_attachment' => $attachment['secureFile']->getData(),
202
                        'download_url' => $attachment['downloadURL']->getData(),
203
                    ];
204
205
                    try {
206
                        $this->attachmentSubmitHandler->handleFormSubmit(
207
                            $attachment->getData(),
208
                            $attachment['file']->getData(),
209
                            $options
210
                        );
211
                    } catch (AttachmentDownloadException $attachmentDownloadException) {
212
                        $this->addFlash(
213
                            'error',
214
                            $this->translator->trans(
215
                                'attachment.download_failed'
216
                            ).' '.$attachmentDownloadException->getMessage()
217
                        );
218
                    }
219
                }
220
221
                $this->commentHelper->setMessage($form['log_comment']->getData());
222
223
                $em->persist($entity);
224
                $em->flush();
225
                $this->addFlash('success', 'entity.edit_flash');
226
            }
227
228
            //Rebuild form, so it is based on the updated data. Important for the parent field!
229
            //We can not use dynamic form events here, because the parent entity list is build from database!
230
            $form = $this->createForm($this->form_class, $entity, [
231
                'attachment_class' => $this->attachment_class,
232
                'parameter_class' => $this->parameter_class,
233
            ]);
234
        } elseif ($form->isSubmitted() && !$form->isValid()) {
235
            $this->addFlash('error', 'entity.edit_flash.invalid');
236
        }
237
238
        //Show preview for LabelProfile if needed.
239
        if ($entity instanceof LabelProfile) {
240
            $example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
241
            $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
242
        }
243
244
        /** @var AbstractPartsContainingRepository $repo */
245
        $repo = $this->entityManager->getRepository($this->entity_class);
246
247
        return $this->renderForm($this->twig_template, [
248
            'entity' => $entity,
249
            'form' => $form,
250
            'route_base' => $this->route_base,
251
            'datatable' => $table,
252
            'pdf_data' => $pdf_data ?? null,
253
            'timeTravel' => $timeTravel_timestamp,
254
            'repo' => $repo,
255
            'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface,
256
        ]);
257
    }
258
259
    /**
260
     * Perform some additional actions, when the form was valid, but before the entity is saved.
261
     *
262
     * @return bool return true, to save entity normally, return false, to abort saving
263
     */
264
    protected function additionalActionNew(FormInterface $form, AbstractNamedDBElement $entity): bool
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

264
    protected function additionalActionNew(/** @scrutinizer ignore-unused */ FormInterface $form, AbstractNamedDBElement $entity): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
265
    {
266
        return true;
267
    }
268
269
    protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null)
270
    {
271
        $master_picture_backup = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $master_picture_backup is dead and can be removed.
Loading history...
272
        if (null === $entity) {
273
            /** @var AbstractStructuralDBElement|User $new_entity */
274
            $new_entity = new $this->entity_class();
275
        } else {
276
            /** @var AbstractStructuralDBElement|User $new_entity */
277
            $new_entity = clone $entity;
278
        }
279
280
        $this->denyAccessUnlessGranted('read', $new_entity);
281
282
        //Basic edit form
283
        $form = $this->createForm($this->form_class, $new_entity, [
284
            'attachment_class' => $this->attachment_class,
285
            'parameter_class' => $this->parameter_class,
286
        ]);
287
288
        $form->handleRequest($request);
289
290
        if ($form->isSubmitted() && $form->isValid()) {
291
            //Perform additional actions
292
            if ($this->additionalActionNew($form, $new_entity)) {
293
                //Upload passed files
294
                $attachments = $form['attachments'];
295
                foreach ($attachments as $attachment) {
296
                    /** @var FormInterface $attachment */
297
                    $options = [
298
                        'secure_attachment' => $attachment['secureFile']->getData(),
299
                        'download_url' => $attachment['downloadURL']->getData(),
300
                    ];
301
302
                    try {
303
                        $this->attachmentSubmitHandler->handleFormSubmit(
304
                            $attachment->getData(),
305
                            $attachment['file']->getData(),
306
                            $options
307
                        );
308
                    } catch (AttachmentDownloadException $attachmentDownloadException) {
309
                        $this->addFlash(
310
                            'error',
311
                            $this->translator->trans(
312
                                'attachment.download_failed'
313
                            ).' '.$attachmentDownloadException->getMessage()
314
                        );
315
                    }
316
                }
317
318
                $this->commentHelper->setMessage($form['log_comment']->getData());
319
320
                $em->persist($new_entity);
321
                $em->flush();
322
                $this->addFlash('success', 'entity.created_flash');
323
324
                return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
325
            }
326
        }
327
328
        if ($form->isSubmitted() && !$form->isValid()) {
329
            $this->addFlash('error', 'entity.created_flash.invalid');
330
        }
331
332
        //Import form
333
        $import_form = $this->createForm(ImportType::class, ['entity_class' => $this->entity_class]);
334
        $import_form->handleRequest($request);
335
336
        if ($import_form->isSubmitted() && $import_form->isValid()) {
337
            /** @var UploadedFile $file */
338
            $file = $import_form['file']->getData();
339
            $data = $import_form->getData();
340
341
            $options = [
342
                'parent' => $data['parent'],
343
                'preserve_children' => $data['preserve_children'],
344
                'format' => $data['format'],
345
                'csv_separator' => $data['csv_separator'],
346
            ];
347
348
            $this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
349
350
            $errors = $importer->fileToDBEntities($file, $this->entity_class, $options);
351
352
            foreach ($errors as $name => $error) {
353
                /** @var ConstraintViolationList $error */
354
                $this->addFlash('error', $name.':'.$error);
355
            }
356
        }
357
358
        //Mass creation form
359
        $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
360
        $mass_creation_form->handleRequest($request);
361
362
        if ($mass_creation_form->isSubmitted() && $mass_creation_form->isValid()) {
363
            $data = $mass_creation_form->getData();
364
365
            //Create entries based on input
366
            $errors = [];
367
            $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors);
368
369
            //Show errors to user:
370
            foreach ($errors as $error) {
371
                if ($error['entity'] instanceof AbstractStructuralDBElement) {
372
                    $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
373
                } else { //When we dont have a structural element, we can only show the name
374
                    $this->addFlash('error', $error['entity']->getName().':'.$error['violations']);
375
                }
376
            }
377
378
            //Persist valid entities to DB
379
            foreach ($results as $result) {
380
                $em->persist($result);
381
            }
382
            $em->flush();
383
        }
384
385
        return $this->renderForm($this->twig_template, [
386
            'entity' => $new_entity,
387
            'form' => $form,
388
            'import_form' => $import_form,
389
            'mass_creation_form' => $mass_creation_form,
390
            'route_base' => $this->route_base,
391
        ]);
392
    }
393
394
    /**
395
     * Performs checks if the element can be deleted safely. Otherwise an flash message is added.
396
     *
397
     * @param AbstractNamedDBElement $entity the element that should be checked
398
     *
399
     * @return bool True if the the element can be deleted, false if not
400
     */
401
    protected function deleteCheck(AbstractNamedDBElement $entity): bool
402
    {
403
        if ($entity instanceof AbstractPartsContainingDBElement) {
404
            /** @var AbstractPartsContainingRepository $repo */
405
            $repo = $this->entityManager->getRepository($this->entity_class);
406
            if ($repo->getPartsCount($entity) > 0) {
407
                $this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
408
409
                return false;
410
            }
411
        }
412
413
        return true;
414
    }
415
416
    protected function _delete(Request $request, AbstractNamedDBElement $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
417
    {
418
        $this->denyAccessUnlessGranted('delete', $entity);
419
420
        if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
421
422
            $entityManager = $this->entityManager;
423
424
            if (!$this->deleteCheck($entity)) {
425
                return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]);
426
            }
427
428
            //Check if we need to remove recursively
429
            if ($entity instanceof AbstractStructuralDBElement && $request->get('delete_recursive', false)) {
430
                $can_delete = true;
431
                //Check if any of the children can not be deleted, cause it contains parts
432
                $recursionHelper->execute($entity, function (AbstractStructuralDBElement $element) use (&$can_delete) {
433
                    if(!$this->deleteCheck($element)) {
434
                        $can_delete = false;
435
                    }
436
                });
437
                if($can_delete) {
438
                    $recursionHelper->delete($entity, false);
439
                } else {
440
                    return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]);
441
                }
442
            } else {
443
                if ($entity instanceof AbstractStructuralDBElement) {
444
                    $parent = $entity->getParent();
445
446
                    //Move all sub entities to the current parent
447
                    foreach ($entity->getSubelements() as $subelement) {
448
                        $subelement->setParent($parent);
449
                        $entityManager->persist($subelement);
450
                    }
451
                }
452
453
                //Remove current element
454
                $entityManager->remove($entity);
455
            }
456
457
            $this->commentHelper->setMessage($request->request->get('log_comment', null));
458
459
            //Flush changes
460
            $entityManager->flush();
461
462
            $this->addFlash('success', 'attachment_type.deleted');
463
        } else {
464
            $this->addFlash('error', 'csfr_invalid');
465
        }
466
467
        return $this->redirectToRoute($this->route_base.'_new');
468
    }
469
470
    protected function _exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
471
    {
472
        $entity = new $this->entity_class();
473
        $this->denyAccessUnlessGranted('read', $entity);
474
        $entities = $em->getRepository($this->entity_class)->findAll();
475
476
        return $exporter->exportEntityFromRequest($entities, $request);
477
    }
478
479
    protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter $exporter, Request $request): Response
480
    {
481
        $this->denyAccessUnlessGranted('read', $entity);
482
483
        return $exporter->exportEntityFromRequest($entity, $request);
484
    }
485
}
486