Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
29.88% covered (danger)
29.88%
49 / 164
35.00% covered (danger)
35.00%
7 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
CRUDController
29.88% covered (danger)
29.88%
49 / 164
35.00% covered (danger)
35.00%
7 / 20
1137.28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getDomain
n/a
0 / 0
n/a
0 / 0
0
 getModel
n/a
0 / 0
n/a
0 / 0
0
 getModelClass
n/a
0 / 0
n/a
0 / 0
0
 getFormType
n/a
0 / 0
n/a
0 / 0
0
 getTemplatingBasePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFlashbagMessage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getRouteName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getListData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listAction
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 formPrePersistData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createAction
61.90% covered (warning)
61.90%
13 / 21
0.00% covered (danger)
0.00%
0 / 1
6.38
 editAction
66.67% covered (warning)
66.67%
16 / 24
0.00% covered (danger)
0.00%
0 / 1
10.37
 showAction
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
110
 deleteAction
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 deleteConfirmationAction
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 pdfAction
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getPdfName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isSoftDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNotifications
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteAllAction
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 deleteConfirmationAllAction
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 pdfAllAction
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 archiveAllAction
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3/**
4 * This file is part of the MADIS - RGPD Management application.
5 *
6 * @copyright Copyright (c) 2018-2019 Soluris - Solutions Numériques Territoriales Innovantes
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
22declare(strict_types=1);
23
24namespace App\Application\Controller;
25
26use App\Application\DDD\Repository\RepositoryInterface;
27use App\Application\Doctrine\Repository\CRUDRepository;
28use App\Application\Interfaces\CollectivityRelated;
29use App\Application\Symfony\Security\UserProvider;
30use App\Domain\Notification\Model\Notification;
31use App\Domain\Tools\ChainManipulator;
32use App\Domain\User\Model\Collectivity;
33use App\Domain\User\Model\User;
34use Doctrine\ORM\EntityManagerInterface;
35use Knp\Bundle\SnappyBundle\Snappy\Response\PdfResponse;
36use Knp\Snappy\Pdf;
37use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
38use Symfony\Component\HttpFoundation\Request;
39use Symfony\Component\HttpFoundation\Response;
40use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
41use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
42use Symfony\Contracts\Translation\TranslatorInterface;
43use Symfony\Polyfill\Intl\Icu\Exception\MethodNotImplementedException;
44
45abstract class CRUDController extends AbstractController
46{
47    /**
48     * @var EntityManagerInterface
49     */
50    protected $entityManager;
51
52    /**
53     * @var TranslatorInterface
54     */
55    protected $translator;
56
57    /**
58     * @var CRUDRepository
59     */
60    protected $repository;
61
62    /**
63     * @var Pdf
64     */
65    protected $pdf;
66
67    /**
68     * @var UserProvider
69     */
70    protected $userProvider;
71
72    /**
73     * @var AuthorizationCheckerInterface
74     */
75    protected $authorizationChecker;
76
77    /**
78     * CRUDController constructor.
79     */
80    public function __construct(
81        EntityManagerInterface $entityManager,
82        TranslatorInterface $translator,
83        RepositoryInterface $repository,
84        Pdf $pdf,
85        UserProvider $userProvider,
86        AuthorizationCheckerInterface $authorizationChecker,
87    ) {
88        $this->entityManager        = $entityManager;
89        $this->translator           = $translator;
90        $this->repository           = $repository;
91        $this->pdf                  = $pdf;
92        $this->userProvider         = $userProvider;
93        $this->authorizationChecker = $authorizationChecker;
94    }
95
96    /**
97     * Get the domain of the object.
98     * (Formatted as string word).
99     */
100    abstract protected function getDomain(): string;
101
102    /**
103     * Get the model of the object.
104     * (Formatted as string word).
105     */
106    abstract protected function getModel(): string;
107
108    /**
109     * Get the model class name of the object.
110     * This methods return the class name with it namespace.
111     */
112    abstract protected function getModelClass(): string;
113
114    /**
115     * Get the form type class name to use during create & edit action.
116     */
117    abstract protected function getFormType(): string;
118
119    /**
120     * Generate the templating base path dynamically depending on the domain, model & template.
121     *
122     * @param string|null $template The template to display
123     *
124     * @return string The generated templating base path
125     */
126    protected function getTemplatingBasePath(?string $template = null): string
127    {
128        // TODO: Check template existence
129        $domain = \ucfirst($this->getDomain());
130        $model  = \ucfirst($this->getModel());
131
132        return "{$domain}/{$model}/{$template}.html.twig";
133    }
134
135    /**
136     * Generate the flashbag message dynamically depending on the domain, model & object.
137     * Replace word `%object%` in translation by the related object (thanks to it `__toString` method).
138     *
139     * @param string      $type     The flashbag type
140     * @param string|null $template The related template to use
141     * @param mixed|null  $object   The object to use to generate flashbag (eg. show object name)
142     *
143     * @return string The generated flashbag
144     */
145    protected function getFlashbagMessage(string $type, ?string $template = null, $object = null): string
146    {
147        $params = [];
148        if (!\is_null($object)) {
149            $params['%object%'] = $object;
150        }
151
152        return $this->translator->trans(
153            "{$this->getDomain()}.{$this->getModel()}.flashbag.{$type}.{$template}",
154            $params
155        );
156    }
157
158    /**
159     * Generate route name depending on the template.
160     *
161     * @param string|null $template The template to use for generation
162     *
163     * @return string The generated route name
164     */
165    protected function getRouteName(?string $template = null): string
166    {
167        return "{$this->getDomain()}_{$this->getModel()}_{$template}";
168    }
169
170    /**
171     * Get data to use in List view.
172     *
173     * @return array
174     */
175    protected function getListData()
176    {
177        return $this->repository->findAll();
178    }
179
180    /**
181     * The list action view
182     * Get data & display them.
183     */
184    public function listAction(): Response
185    {
186        return $this->render($this->getTemplatingBasePath('list'), [
187            'objects' => $this->getListData(),
188        ]);
189    }
190
191    /**
192     * Actions to make when a form is submitted and valid.
193     * This method is handled just after form validation, before object manipulation.
194     */
195    public function formPrePersistData($object, $form = null)
196    {
197    }
198
199    /**
200     * The creation action view
201     * Create a new data.
202     */
203    public function createAction(Request $request): Response
204    {
205        $modelClass = $this->getModelClass();
206        /** @var CollectivityRelated $object */
207        $object         = new $modelClass();
208        $serviceEnabled = false;
209
210        if ($object instanceof CollectivityRelated) {
211            $user = $this->userProvider->getAuthenticatedUser();
212            $object->setCollectivity($user->getCollectivity());
213            $serviceEnabled = $object->getCollectivity() && $object->getCollectivity()->getIsServicesEnabled();
214        }
215
216        $form = $this->createForm($this->getFormType(), $object, ['validation_groups' => ['default', $this->getModel(), 'create']]);
217
218        $form->handleRequest($request);
219
220        if ($form->isSubmitted() && $form->isValid()) {
221            $this->formPrePersistData($object, $form);
222            $em = $this->getDoctrine()->getManager();
223
224            $em->persist($object);
225            $em->flush();
226
227            $this->addFlash('success', $this->getFlashbagMessage('success', 'create', $object));
228
229            return $this->redirectToRoute($this->getRouteName('list'));
230        }
231
232        return $this->render($this->getTemplatingBasePath('create'), [
233            'form'           => $form->createView(),
234            'object'         => $object,
235            'serviceEnabled' => $serviceEnabled,
236        ]);
237    }
238
239    /**
240     * The edition action view
241     * Edit an existing data.
242     *
243     * @param string $id The ID of the data to edit
244     */
245    public function editAction(Request $request, string $id): Response
246    {
247        //        /** @var CollectivityRelated $object */
248        $object = $this->repository->findOneById($id);
249        if (!$object) {
250            throw new NotFoundHttpException("No object found with ID '{$id}'");
251        }
252
253        $serviceEnabled = false;
254
255        /** @var User $user */
256        $user = $this->getUser();
257        if (!$user->hasAccessTo($object)) {
258            return $this->redirectToRoute($this->getRouteName('list'));
259        }
260
261        if ($object instanceof Collectivity) {
262            $serviceEnabled = $object->getIsServicesEnabled();
263        } elseif ($object instanceof CollectivityRelated) {
264            $serviceEnabled = $object->getCollectivity() && $object->getCollectivity()->getIsServicesEnabled();
265        }
266
267        $form = $this->createForm($this->getFormType(), $object, ['validation_groups' => ['default', $this->getModel(), 'edit']]);
268
269        $form->handleRequest($request);
270
271        if ($form->isSubmitted() && $form->isValid()) {
272            $this->formPrePersistData($object, $form);
273            $this->entityManager->persist($object);
274            $this->entityManager->flush();
275
276            $this->addFlash('success', $this->getFlashbagMessage('success', 'edit', $object));
277
278            return $this->redirectToRoute($this->getRouteName('list'));
279        }
280
281        return $this->render($this->getTemplatingBasePath('edit'), [
282            'form'           => $form->createView(),
283            'object'         => $object,
284            'serviceEnabled' => $serviceEnabled,
285        ]);
286    }
287
288    /**
289     * The show action view
290     * Display the object information.
291     *
292     * @param string $id The ID of the data to display
293     */
294    public function showAction(string $id): Response
295    {
296        /** @var CollectivityRelated $object */
297        $object = $this->repository->findOneById($id);
298        if (!$object) {
299            throw new NotFoundHttpException("No object found with ID '{$id}'");
300        }
301        /**
302         * @var User $user
303         */
304        $user = $this->getUser();
305        if (!$user->hasAccessTo($object, false)) {
306            throw $this->createAccessDeniedException();
307        }
308        $serviceEnabled = false;
309        if ($object instanceof Collectivity) {
310            $serviceEnabled = $object->getIsServicesEnabled();
311        } elseif ($object instanceof CollectivityRelated && $object->getCollectivity()) {
312            $serviceEnabled = $object->getCollectivity()->getIsServicesEnabled();
313        }
314
315        $actionEnabled = true;
316
317        if ($object instanceof CollectivityRelated && !$this->authorizationChecker->isGranted('ROLE_ADMIN') && !$user->getServices()->isEmpty()) {
318            $actionEnabled = $object->isInUserServices($this->userProvider->getAuthenticatedUser());
319        }
320
321        if (!$this->isGranted('ROLE_USER')) {
322            $actionEnabled = false;
323        }
324
325        return $this->render($this->getTemplatingBasePath('show'), [
326            'object'         => $object,
327            'actionEnabled'  => $actionEnabled,
328            'serviceEnabled' => $serviceEnabled,
329        ]);
330    }
331
332    /**
333     * The delete action view
334     * Display a confirmation message to confirm data deletion.
335     */
336    public function deleteAction(string $id): Response
337    {
338        /** @var CollectivityRelated $object */
339        $object = $this->repository->findOneById($id);
340        if (!$object) {
341            throw new NotFoundHttpException("No object found with ID '{$id}'");
342        }
343        /**
344         * @var User $user
345         */
346        $user = $this->getUser();
347
348        if (!$user->hasAccessTo($object)) {
349            return $this->redirectToRoute($this->getRouteName('list'));
350        }
351
352        return $this->render($this->getTemplatingBasePath('delete'), [
353            'object' => $object,
354            'id'     => $id,
355        ]);
356    }
357
358    /**
359     * The deletion action
360     * Delete the data.
361     *
362     * @throws \Exception
363     */
364    public function deleteConfirmationAction(string $id): Response
365    {
366        $object = $this->repository->findOneById($id);
367        if (!$object) {
368            throw new NotFoundHttpException("No object found with ID '{$id}'");
369        }
370        /**
371         * @var User $user
372         */
373        $user = $this->getUser();
374        if (!$user->hasAccessTo($object)) {
375            return $this->redirectToRoute($this->getRouteName('list'));
376        }
377
378        if ($this->isSoftDelete()) {
379            if (!\method_exists($object, 'setDeletedAt')) {
380                throw new MethodNotImplementedException('setDeletedAt');
381            }
382            $object->setDeletedAt(new \DateTimeImmutable());
383            $this->repository->update($object);
384        } else {
385            $this->entityManager->remove($object);
386            $this->entityManager->flush();
387        }
388
389        $this->addFlash('success', $this->getFlashbagMessage('success', 'delete', $object));
390
391        return $this->redirectToRoute($this->getRouteName('list'));
392    }
393
394    public function pdfAction(string $id)
395    {
396        $object = $this->repository->findOneById($id);
397        if (!$object) {
398            throw new NotFoundHttpException("No object found with ID '{$id}'");
399        }
400        /** @var User $user */
401        $user = $this->getUser();
402        if (!$user->hasAccessTo($object, false)) {
403            return $this->redirectToRoute($this->getRouteName('list'));
404        }
405
406        return new PdfResponse(
407            $this->pdf->getOutputFromHtml(
408                $this->renderView($this->getTemplatingBasePath('pdf'), ['object' => $object])
409            ),
410            $this->getPdfName((string) $object) . '.pdf'
411        );
412    }
413
414    public function getPdfName(string $name): string
415    {
416        $name = ChainManipulator::removeAllNonAlphaNumericChar(ChainManipulator::removeAccents($name));
417
418        return $name . '-' . date('mdY');
419    }
420
421    /**
422     * Check if we have to produce a soft delete behaviour.
423     */
424    protected function isSoftDelete(): bool
425    {
426        return false;
427    }
428
429    public function getNotifications(): array
430    {
431        return $this->entityManager->getRepository(Notification::class)->findAll();
432    }
433
434    /**
435     * The delete action list
436     * Display a confirmation message to confirm data deletion.
437     */
438    public function deleteAllAction(Request $request): Response
439    {
440        $ids = $request->query->get('ids');
441        $ids = explode(',', $ids);
442
443        return $this->render($this->getTemplatingBasePath('delete_all'), [ // delete_all
444            'ids'            => $ids,
445            'objects_length' => count($ids),
446        ]);
447    }
448
449    public function deleteConfirmationAllAction(Request $request): Response
450    {
451        $ids = $request->query->get('ids');
452
453        foreach ($ids as $id) {
454            $this->deleteConfirmationAction($id);
455        }
456
457        return $this->redirectToRoute($this->getRouteName('list'));
458    }
459
460    // RETURN A PDF WITH ALL IDS FILTERED
461    public function pdfAllAction(Request $request)
462    {
463        $ids = $request->query->get('ids');
464        $ids = explode(',', $ids);
465
466        $objects = [];
467
468        foreach ($ids as $id) {
469            $object = $this->repository->findOneById($id);
470            if ($this->userProvider->getAuthenticatedUser()->hasAccessTo($object, false)) {
471                array_push($objects, $object);
472            }
473        }
474
475        return new PdfResponse(
476            $this->pdf->getOutputFromHtml(
477                $this->renderView($this->getTemplatingBasePath('pdf_all'), ['objects' => $objects])
478            ),
479            $this->getPdfName((string) 'print') . '.pdf'
480        );
481    }
482
483    /**
484     * The archive action
485     * Display a confirmation message to confirm data archived.
486     */
487    public function archiveAllAction(Request $request): Response
488    {
489        $ids = $request->query->get('ids');
490        $ids = explode(',', $ids);
491
492        if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
493            $this->addFlash('error', 'Vous ne pouvez pas archiver ces traitements');
494
495            return $this->redirectToRoute($this->getRouteName('list'));
496        }
497
498        foreach ($ids as $id) {
499            $object = $this->repository->findOneById($id);
500            if ($object && $this->userProvider->getAuthenticatedUser()->hasAccessTo($object)) {
501                $object->setActive(false);
502                $this->addFlash('success', $this->getFlashbagMessage('success', 'delete', $object));
503            }
504        }
505        $this->entityManager->flush();
506
507        return $this->redirectToRoute($this->getRouteName('list'));
508    }
509}