Passed
Push — master ( be4768...895194 )
by Jan
09:55
created

BaseAdminController   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 216
dl 0
loc 383
rs 4.08
c 1
b 0
f 0
wmc 59

6 Methods

Rating   Name   Duplication   Size   Complexity  
C _delete() 0 65 15
C _new() 0 117 17
D _edit() 0 120 18
A _exportEntity() 0 5 1
A _exportAll() 0 9 1
B __construct() 0 29 7

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 - 2020 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
/**
24
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25
 *
26
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software; you can redistribute it and/or
29
 * modify it under the terms of the GNU General Public License
30
 * as published by the Free Software Foundation; either version 2
31
 * of the License, or (at your option) any later version.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 * GNU General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU General Public License
39
 * along with this program; if not, write to the Free Software
40
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
41
 */
42
43
namespace App\Controller\AdminPages;
44
45
use App\DataTables\LogDataTable;
46
use App\Entity\Attachments\AttachmentType;
47
use App\Entity\Base\AbstractNamedDBElement;
48
use App\Entity\Base\AbstractPartsContainingDBElement;
49
use App\Entity\Base\AbstractStructuralDBElement;
50
use App\Entity\Base\PartsContainingRepositoryInterface;
51
use App\Entity\LabelSystem\LabelProfile;
52
use App\Entity\PriceInformations\Currency;
53
use App\Entity\UserSystem\Group;
54
use App\Entity\UserSystem\User;
55
use App\Events\SecurityEvent;
56
use App\Events\SecurityEvents;
57
use App\Exceptions\AttachmentDownloadException;
58
use App\Form\AdminPages\ImportType;
59
use App\Form\AdminPages\MassCreationForm;
60
use App\Repository\AbstractPartsContainingRepository;
61
use App\Services\Attachments\AttachmentSubmitHandler;
62
use App\Services\EntityExporter;
63
use App\Services\EntityImporter;
64
use App\Services\LabelSystem\Barcodes\BarcodeExampleElementsGenerator;
65
use App\Services\LabelSystem\LabelGenerator;
66
use App\Services\LogSystem\EventCommentHelper;
67
use App\Services\LogSystem\HistoryHelper;
68
use App\Services\LogSystem\TimeTravel;
69
use App\Services\StructuralElementRecursionHelper;
70
use Doctrine\ORM\EntityManagerInterface;
71
use InvalidArgumentException;
72
use Omines\DataTablesBundle\DataTableFactory;
73
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
74
use Symfony\Component\EventDispatcher\EventDispatcher;
75
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
76
use Symfony\Component\Form\FormInterface;
77
use Symfony\Component\HttpFoundation\File\UploadedFile;
78
use Symfony\Component\HttpFoundation\RedirectResponse;
79
use Symfony\Component\HttpFoundation\Request;
80
use Symfony\Component\HttpFoundation\Response;
81
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
82
use Symfony\Component\Validator\ConstraintViolationList;
83
use Symfony\Contracts\Translation\TranslatorInterface;
84
85
abstract class BaseAdminController extends AbstractController
86
{
87
    protected $entity_class = '';
88
    protected $form_class = '';
89
    protected $twig_template = '';
90
    protected $route_base = '';
91
    protected $attachment_class = '';
92
    protected $parameter_class = '';
93
94
    protected $passwordEncoder;
95
    protected $translator;
96
    protected $attachmentSubmitHandler;
97
    protected $commentHelper;
98
99
    protected $historyHelper;
100
    protected $timeTravel;
101
    protected $dataTableFactory;
102
    /**
103
     * @var EventDispatcher
104
     */
105
    protected $eventDispatcher;
106
    protected $labelGenerator;
107
    protected $barcodeExampleGenerator;
108
109
    protected $entityManager;
110
111
    public function __construct(TranslatorInterface $translator, UserPasswordEncoderInterface $passwordEncoder,
112
        AttachmentSubmitHandler $attachmentSubmitHandler,
113
        EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
114
        DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, BarcodeExampleElementsGenerator $barcodeExampleGenerator,
115
        LabelGenerator $labelGenerator, EntityManagerInterface $entityManager)
116
    {
117
        if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
0 ignored issues
show
introduced by
The condition '' === $this->route_base is always false.
Loading history...
introduced by
The condition '' === $this->twig_template is always false.
Loading history...
introduced by
The condition '' === $this->form_class is always false.
Loading history...
118
            throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
119
        }
120
121
        if ('' === $this->attachment_class) {
0 ignored issues
show
introduced by
The condition '' === $this->attachment_class is always false.
Loading history...
122
            throw new InvalidArgumentException('You have to override the $attachment_class value in your subclass!');
123
        }
124
125
        if ('' === $this->parameter_class) {
0 ignored issues
show
introduced by
The condition '' === $this->parameter_class is always false.
Loading history...
126
            throw new InvalidArgumentException('You have to override the $parameter_class value in your subclass!');
127
        }
128
129
        $this->translator = $translator;
130
        $this->passwordEncoder = $passwordEncoder;
131
        $this->attachmentSubmitHandler = $attachmentSubmitHandler;
132
        $this->commentHelper = $commentHelper;
133
        $this->historyHelper = $historyHelper;
134
        $this->timeTravel = $timeTravel;
135
        $this->dataTableFactory = $dataTableFactory;
136
        $this->eventDispatcher = $eventDispatcher;
137
        $this->barcodeExampleGenerator = $barcodeExampleGenerator;
138
        $this->labelGenerator = $labelGenerator;
139
        $this->entityManager = $entityManager;
140
    }
141
142
    protected function _edit(AbstractNamedDBElement $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
143
    {
144
        $this->denyAccessUnlessGranted('read', $entity);
145
146
        $timeTravel_timestamp = null;
147
        if (null !== $timestamp) {
148
            $this->denyAccessUnlessGranted('@tools.timetravel');
149
            $this->denyAccessUnlessGranted('show_history', $entity);
150
            //If the timestamp only contains numbers interpret it as unix timestamp
151
            if (ctype_digit($timestamp)) {
152
                $timeTravel_timestamp = new \DateTime();
153
                $timeTravel_timestamp->setTimestamp((int) $timestamp);
154
            } else { //Try to parse it via DateTime
155
                $timeTravel_timestamp = new \DateTime($timestamp);
156
            }
157
            $this->timeTravel->revertEntityToTimestamp($entity, $timeTravel_timestamp);
158
        }
159
160
        if ($this->isGranted('show_history', $entity)) {
161
            $table = $this->dataTableFactory->createFromType(
162
                LogDataTable::class,
163
                [
164
                    'filter_elements' => $this->historyHelper->getAssociatedElements($entity),
165
                    'mode' => 'element_history',
166
                ],
167
                ['pageLength' => 10]
168
            )
169
                ->handleRequest($request);
170
171
            if ($table->isCallback()) {
172
                return $table->getResponse();
173
            }
174
        } else {
175
            $table = null;
176
        }
177
178
        $form_options = [
179
            'attachment_class' => $this->attachment_class,
180
            'parameter_class' => $this->parameter_class,
181
            'disabled' => null !== $timeTravel_timestamp ? true : false,
182
        ];
183
184
        //Disable editing of options, if user is not allowed to use twig...
185
        if (
186
            $entity instanceof LabelProfile
187
            && 'twig' === $entity->getOptions()->getLinesMode()
188
            && ! $this->isGranted('@labels.use_twig')
189
        ) {
190
            $form_options['disable_options'] = true;
191
        }
192
193
        $form = $this->createForm($this->form_class, $entity, $form_options);
194
195
        $form->handleRequest($request);
196
        if ($form->isSubmitted() && $form->isValid()) {
197
            //Check if we editing a user and if we need to change the password of it
198
            if ($entity instanceof User && ! empty($form['new_password']->getData())) {
199
                $password = $this->passwordEncoder->encodePassword($entity, $form['new_password']->getData());
200
                $entity->setPassword($password);
201
                //By default the user must change the password afterwards
202
                $entity->setNeedPwChange(true);
203
204
                $event = new SecurityEvent($entity);
205
                $this->eventDispatcher->dispatch($event, SecurityEvents::PASSWORD_CHANGED);
206
            }
207
208
            //Upload passed files
209
            $attachments = $form['attachments'];
210
            foreach ($attachments as $attachment) {
211
                /** @var FormInterface $attachment */
212
                $options = [
213
                    'secure_attachment' => $attachment['secureFile']->getData(),
214
                    'download_url' => $attachment['downloadURL']->getData(),
215
                ];
216
217
                try {
218
                    $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
219
                } catch (AttachmentDownloadException $attachmentDownloadException) {
220
                    $this->addFlash(
221
                        'error',
222
                        $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
223
                    );
224
                }
225
            }
226
227
            $this->commentHelper->setMessage($form['log_comment']->getData());
228
229
            $em->persist($entity);
230
            $em->flush();
231
            $this->addFlash('success', 'entity.edit_flash');
232
233
            //Rebuild form, so it is based on the updated data. Important for the parent field!
234
            //We can not use dynamic form events here, because the parent entity list is build from database!
235
            $form = $this->createForm($this->form_class, $entity, [
236
                'attachment_class' => $this->attachment_class,
237
                'parameter_class' => $this->parameter_class,
238
            ]);
239
        } elseif ($form->isSubmitted() && ! $form->isValid()) {
240
            $this->addFlash('error', 'entity.edit_flash.invalid');
241
        }
242
243
        //Show preview for LabelProfile if needed.
244
        if ($entity instanceof LabelProfile) {
245
            $example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
246
            $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
247
        }
248
249
        /** @var AbstractPartsContainingRepository $repo */
250
        $repo = $this->entityManager->getRepository($this->entity_class);
251
252
253
        return $this->render($this->twig_template, [
254
            'entity' => $entity,
255
            'form' => $form->createView(),
256
            'route_base' => $this->route_base,
257
            'datatable' => $table,
258
            'pdf_data' => $pdf_data ?? null,
259
            'timeTravel' => $timeTravel_timestamp,
260
            'repo' => $repo,
261
            'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface,
262
        ]);
263
    }
264
265
    protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null)
266
    {
267
        $master_picture_backup = null;
268
        if (null === $entity) {
269
            /** @var AbstractStructuralDBElement|User $new_entity */
270
            $new_entity = new $this->entity_class();
271
        } else {
272
            /** @var AbstractStructuralDBElement|User $new_entity */
273
            $new_entity = clone $entity;
274
        }
275
276
        $this->denyAccessUnlessGranted('read', $new_entity);
277
278
        //Basic edit form
279
        $form = $this->createForm($this->form_class, $new_entity, [
280
            'attachment_class' => $this->attachment_class,
281
            'parameter_class' => $this->parameter_class,
282
        ]);
283
284
        $form->handleRequest($request);
285
286
        if ($form->isSubmitted() && $form->isValid()) {
287
            if ($new_entity instanceof User && ! empty($form['new_password']->getData())) {
288
                $password = $this->passwordEncoder->encodePassword($new_entity, $form['new_password']->getData());
289
                $new_entity->setPassword($password);
290
                //By default the user must change the password afterwards
291
                $new_entity->setNeedPwChange(true);
292
            }
293
294
            //Upload passed files
295
            $attachments = $form['attachments'];
296
            foreach ($attachments as $attachment) {
297
                /** @var FormInterface $attachment */
298
                $options = [
299
                    'secure_attachment' => $attachment['secureFile']->getData(),
300
                    'download_url' => $attachment['downloadURL']->getData(),
301
                ];
302
303
                try {
304
                    $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
305
                } catch (AttachmentDownloadException $attachmentDownloadException) {
306
                    $this->addFlash(
307
                        'error',
308
                        $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
309
                    );
310
                }
311
            }
312
313
            $this->commentHelper->setMessage($form['log_comment']->getData());
314
315
            $em->persist($new_entity);
316
            $em->flush();
317
            $this->addFlash('success', 'entity.created_flash');
318
319
            return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
320
        }
321
322
        if ($form->isSubmitted() && ! $form->isValid()) {
323
            $this->addFlash('error', 'entity.created_flash.invalid');
324
        }
325
326
        //Import form
327
        $import_form = $this->createForm(ImportType::class, ['entity_class' => $this->entity_class]);
328
        $import_form->handleRequest($request);
329
330
        if ($import_form->isSubmitted() && $import_form->isValid()) {
331
            /** @var UploadedFile $file */
332
            $file = $import_form['file']->getData();
333
            $data = $import_form->getData();
334
335
            $options = [
336
                'parent' => $data['parent'],
337
                'preserve_children' => $data['preserve_children'],
338
                'format' => $data['format'],
339
                'csv_separator' => $data['csv_separator'],
340
            ];
341
342
            $this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
343
344
            $errors = $importer->fileToDBEntities($file, $this->entity_class, $options);
345
346
            foreach ($errors as $name => $error) {
347
                /** @var ConstraintViolationList $error */
348
                $this->addFlash('error', $name.':'.$error);
349
            }
350
        }
351
352
        //Mass creation form
353
        $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
354
        $mass_creation_form->handleRequest($request);
355
356
        if ($mass_creation_form->isSubmitted() && $mass_creation_form->isValid()) {
357
            $data = $mass_creation_form->getData();
358
359
            //Create entries based on input
360
            $errors = [];
361
            $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'], $errors);
362
363
            //Show errors to user:
364
            foreach ($errors as $error) {
365
                $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
366
            }
367
368
            //Persist valid entities to DB
369
            foreach ($results as $result) {
370
                $em->persist($result);
371
            }
372
            $em->flush();
373
        }
374
375
376
        return $this->render($this->twig_template, [
377
            'entity' => $new_entity,
378
            'form' => $form->createView(),
379
            'import_form' => $import_form->createView(),
380
            'mass_creation_form' => $mass_creation_form->createView(),
381
            'route_base' => $this->route_base,
382
        ]);
383
    }
384
385
    protected function _delete(Request $request, AbstractNamedDBElement $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
386
    {
387
        $this->denyAccessUnlessGranted('delete', $entity);
388
389
        if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
390
            $entityManager = $this->getDoctrine()->getManager();
391
392
            //Check if we can delete the part (it must not contain Parts)
393
            if ($entity instanceof AbstractPartsContainingDBElement) {
394
                /** @var AbstractPartsContainingRepository $repo */
395
                $repo = $this->entityManager->getRepository($this->entity_class);
396
                if ($repo->getPartsCount($entity) > 0) {
397
                    $this->addFlash('error', 'entity.delete.must_not_contain_parts');
398
                    return $this->redirectToRoute($this->route_base.'_new');
399
                }
400
            } elseif ($entity instanceof AttachmentType) {
401
                if ($entity->getAttachmentsForType()->count() > 0) {
402
                    $this->addFlash('error', 'entity.delete.must_not_contain_attachments');
403
                    return $this->redirectToRoute($this->route_base.'_new');
404
                }
405
            } elseif ($entity instanceof Currency) {
406
                if ($entity->getPricedetails()->count() > 0) {
407
                    $this->addFlash('error', 'entity.delete.must_not_contain_prices');
408
                    return $this->redirectToRoute($this->route_base.'_new');
409
                }
410
            } elseif ($entity instanceof Group) {
411
                if ($entity->getUsers()->count() > 0) {
412
                    $this->addFlash('error', 'entity.delete.must_not_contain_users');
413
                    return $this->redirectToRoute($this->route_base.'_new');
414
                }
415
            } elseif ($entity instanceof User) {
416
                //TODO: Find a better solution
417
                $this->addFlash('error', 'Currently it is not possible to delete a user, as this would break the log... This will be implemented later...');
418
                return $this->redirectToRoute($this->route_base.'_new');
419
            }
420
421
            //Check if we need to remove recursively
422
            if ($entity instanceof AbstractStructuralDBElement && $request->get('delete_recursive', false)) {
423
                $recursionHelper->delete($entity, false);
424
            } else {
425
                if ($entity instanceof AbstractStructuralDBElement) {
426
                    $parent = $entity->getParent();
427
428
                    //Move all sub entities to the current parent
429
                    foreach ($entity->getSubelements() as $subelement) {
430
                        $subelement->setParent($parent);
431
                        $entityManager->persist($subelement);
432
                    }
433
                }
434
435
                //Remove current element
436
                $entityManager->remove($entity);
437
            }
438
439
            $this->commentHelper->setMessage($request->request->get('log_comment', null));
440
441
            //Flush changes
442
            $entityManager->flush();
443
444
            $this->addFlash('success', 'attachment_type.deleted');
445
        } else {
446
            $this->addFlash('error', 'csfr_invalid');
447
        }
448
449
        return $this->redirectToRoute($this->route_base.'_new');
450
    }
451
452
    protected function _exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
453
    {
454
        $entity = new $this->entity_class();
455
456
        $this->denyAccessUnlessGranted('read', $entity);
457
458
        $entities = $em->getRepository($this->entity_class)->findAll();
459
460
        return $exporter->exportEntityFromRequest($entities, $request);
461
    }
462
463
    protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter $exporter, Request $request): \Symfony\Component\HttpFoundation\Response
464
    {
465
        $this->denyAccessUnlessGranted('read', $entity);
466
467
        return $exporter->exportEntityFromRequest($entity, $request);
468
    }
469
}
470