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

BaseAdminController::__construct()   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 17
c 0
b 0
f 0
nc 4
nop 11
dl 0
loc 29
rs 7.6666

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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