PartController::show()   A
last analyzed

Complexity

Conditions 5
Paths 9

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 30
c 0
b 0
f 0
nc 9
nop 8
dl 0
loc 43
rs 9.1288

How to fix   Many Parameters   

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;
24
25
use App\DataTables\LogDataTable;
26
use App\Entity\Parts\Category;
27
use App\Entity\Parts\Footprint;
28
use App\Entity\Parts\Manufacturer;
29
use App\Entity\Parts\Part;
30
use App\Entity\Parts\PartLot;
31
use App\Entity\Parts\Storelocation;
32
use App\Entity\Parts\Supplier;
33
use App\Entity\PriceInformations\Orderdetail;
34
use App\Entity\ProjectSystem\Project;
35
use App\Exceptions\AttachmentDownloadException;
36
use App\Form\Part\PartBaseType;
37
use App\Services\Attachments\AttachmentSubmitHandler;
38
use App\Services\Attachments\PartPreviewGenerator;
39
use App\Services\LogSystem\EventCommentHelper;
40
use App\Services\LogSystem\HistoryHelper;
41
use App\Services\LogSystem\TimeTravel;
42
use App\Services\Parameters\ParameterExtractor;
43
use App\Services\Parts\PartLotWithdrawAddHelper;
44
use App\Services\Parts\PricedetailHelper;
45
use App\Services\ProjectSystem\ProjectBuildPartHelper;
46
use DateTime;
47
use Doctrine\ORM\EntityManagerInterface;
48
use Exception;
49
use Omines\DataTablesBundle\DataTableFactory;
50
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
51
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
52
use Symfony\Component\Form\FormInterface;
53
use Symfony\Component\HttpFoundation\RedirectResponse;
54
use Symfony\Component\HttpFoundation\Request;
55
use Symfony\Component\HttpFoundation\Response;
56
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
57
use Symfony\Component\Routing\Annotation\Route;
58
use Symfony\Contracts\Translation\TranslatorInterface;
59
60
/**
61
 * @Route("/part")
62
 */
63
class PartController extends AbstractController
64
{
65
    protected PricedetailHelper $pricedetailHelper;
66
    protected PartPreviewGenerator $partPreviewGenerator;
67
    protected EventCommentHelper $commentHelper;
68
69
    public function __construct(PricedetailHelper $pricedetailHelper,
70
        PartPreviewGenerator $partPreviewGenerator, EventCommentHelper $commentHelper)
71
    {
72
        $this->pricedetailHelper = $pricedetailHelper;
73
        $this->partPreviewGenerator = $partPreviewGenerator;
74
        $this->commentHelper = $commentHelper;
75
    }
76
77
    /**
78
     * @Route("/{id}/info/{timestamp}", name="part_info")
79
     * @Route("/{id}", requirements={"id"="\d+"})
80
     *
81
     * @throws Exception
82
     */
83
    public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper,
84
        DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response
85
    {
86
        $this->denyAccessUnlessGranted('read', $part);
87
88
        $timeTravel_timestamp = null;
89
        if (null !== $timestamp) {
90
            $this->denyAccessUnlessGranted('show_history', $part);
91
            //If the timestamp only contains numbers interpret it as unix timestamp
92
            if (ctype_digit($timestamp)) {
93
                $timeTravel_timestamp = new DateTime();
94
                $timeTravel_timestamp->setTimestamp((int) $timestamp);
95
            } else { //Try to parse it via DateTime
96
                $timeTravel_timestamp = new DateTime($timestamp);
97
            }
98
            $timeTravel->revertEntityToTimestamp($part, $timeTravel_timestamp);
99
        }
100
101
        if ($this->isGranted('show_history', $part)) {
102
            $table = $dataTable->createFromType(LogDataTable::class, [
103
                'filter_elements' => $historyHelper->getAssociatedElements($part),
104
                'mode' => 'element_history',
105
            ], ['pageLength' => 10])
106
                ->handleRequest($request);
107
108
            if ($table->isCallback()) {
109
                return $table->getResponse();
110
            }
111
        } else {
112
            $table = null;
113
        }
114
115
        return $this->render(
116
            'parts/info/show_part_info.html.twig',
117
            [
118
                'part' => $part,
119
                'datatable' => $table,
120
                'pricedetail_helper' => $this->pricedetailHelper,
121
                'pictures' => $this->partPreviewGenerator->getPreviewAttachments($part),
122
                'timeTravel' => $timeTravel_timestamp,
123
                'description_params' => $parameterExtractor->extractParameters($part->getDescription()),
124
                'comment_params' => $parameterExtractor->extractParameters($part->getComment()),
125
                'withdraw_add_helper' => $withdrawAddHelper,
126
            ]
127
        );
128
    }
129
130
    /**
131
     * @Route("/{id}/edit", name="part_edit")
132
     */
133
    public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
134
        AttachmentSubmitHandler $attachmentSubmitHandler): Response
135
    {
136
        $this->denyAccessUnlessGranted('edit', $part);
137
138
        $form = $this->createForm(PartBaseType::class, $part);
139
140
        $form->handleRequest($request);
141
        if ($form->isSubmitted() && $form->isValid()) {
142
            //Upload passed files
143
            $attachments = $form['attachments'];
144
            foreach ($attachments as $attachment) {
145
                /** @var FormInterface $attachment */
146
                $options = [
147
                    'secure_attachment' => $attachment['secureFile']->getData(),
148
                    'download_url' => $attachment['downloadURL']->getData(),
149
                ];
150
151
                try {
152
                    $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
153
                } catch (AttachmentDownloadException $attachmentDownloadException) {
154
                    $this->addFlash(
155
                        'error',
156
                        $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
157
                    );
158
                }
159
            }
160
161
            $this->commentHelper->setMessage($form['log_comment']->getData());
162
163
            $em->persist($part);
164
            $em->flush();
165
            $this->addFlash('success', 'part.edited_flash');
166
167
            //Redirect to clone page if user wished that...
168
            //@phpstan-ignore-next-line
169
            if ('save_and_clone' === $form->getClickedButton()->getName()) {
0 ignored issues
show
Bug introduced by
The method getClickedButton() does not exist on Symfony\Component\Form\FormInterface. It seems like you code against a sub-type of Symfony\Component\Form\FormInterface such as Symfony\Component\Form\Form. ( Ignorable by Annotation )

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

169
            if ('save_and_clone' === $form->/** @scrutinizer ignore-call */ getClickedButton()->getName()) {
Loading history...
170
                return $this->redirectToRoute('part_clone', ['id' => $part->getID()]);
171
            }
172
            //@phpstan-ignore-next-line
173
            if ('save_and_new' === $form->getClickedButton()->getName()) {
174
                return $this->redirectToRoute('part_new');
175
            }
176
177
            //Reload form, so the SIUnitType entries use the new part unit
178
            $form = $this->createForm(PartBaseType::class, $part);
179
        } elseif ($form->isSubmitted() && !$form->isValid()) {
180
            $this->addFlash('error', 'part.edited_flash.invalid');
181
        }
182
183
        return $this->renderForm('parts/edit/edit_part_info.html.twig',
184
            [
185
                'part' => $part,
186
                'form' => $form,
187
            ]);
188
    }
189
190
    /**
191
     * @Route("/{id}/delete", name="part_delete", methods={"DELETE"})
192
     */
193
    public function delete(Request $request, Part $part, EntityManagerInterface $entityManager): RedirectResponse
194
    {
195
        $this->denyAccessUnlessGranted('delete', $part);
196
197
        if ($this->isCsrfTokenValid('delete'.$part->getId(), $request->request->get('_token'))) {
198
199
            $this->commentHelper->setMessage($request->request->get('log_comment', null));
200
201
            //Remove part
202
            $entityManager->remove($part);
203
204
            //Flush changes
205
            $entityManager->flush();
206
207
            $this->addFlash('success', 'part.deleted');
208
        }
209
210
        return $this->redirectToRoute('homepage');
211
    }
212
213
    /**
214
     * @Route("/new", name="part_new")
215
     * @Route("/{id}/clone", name="part_clone")
216
     * @Route("/new_build_part/{project_id}", name="part_new_build_part")
217
     * @ParamConverter("part", options={"id" = "id"})
218
     * @ParamConverter("project", options={"id" = "project_id"})
219
     */
220
    public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
221
        AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
222
        ?Part $part = null, ?Project $project = null): Response
223
    {
224
225
        if ($part) { //Clone part
226
            $new_part = clone $part;
227
        } else if ($project) { //Initialize a new part for a build part from the given project
228
            //Ensure that the project has not already a build part
229
            if ($project->getBuildPart() !== null) {
230
                $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists');
231
                return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]);
232
            }
233
            $new_part = $projectBuildPartHelper->getPartInitialization($project);
234
        } else { //Create an empty part from scratch
235
            $new_part = new Part();
236
        }
237
238
        $this->denyAccessUnlessGranted('create', $new_part);
239
240
        $cid = $request->get('category', null);
241
        $category = $cid ? $em->find(Category::class, $cid) : null;
242
        if (null !== $category && null === $new_part->getCategory()) {
243
            $new_part->setCategory($category);
244
            $new_part->setDescription($category->getDefaultDescription());
245
            $new_part->setComment($category->getDefaultComment());
246
        }
247
248
        $fid = $request->get('footprint', null);
249
        $footprint = $fid ? $em->find(Footprint::class, $fid) : null;
250
        if (null !== $footprint && null === $new_part->getFootprint()) {
251
            $new_part->setFootprint($footprint);
252
        }
253
254
        $mid = $request->get('manufacturer', null);
255
        $manufacturer = $mid ? $em->find(Manufacturer::class, $mid) : null;
256
        if (null !== $manufacturer && null === $new_part->getManufacturer()) {
257
            $new_part->setManufacturer($manufacturer);
258
        }
259
260
        $store_id = $request->get('storelocation', null);
261
        $storelocation = $store_id ? $em->find(Storelocation::class, $store_id) : null;
262
        if (null !== $storelocation && $new_part->getPartLots()->isEmpty()) {
263
            $partLot = new PartLot();
264
            $partLot->setStorageLocation($storelocation);
265
            $partLot->setInstockUnknown(true);
266
            $new_part->addPartLot($partLot);
267
        }
268
269
        $supplier_id = $request->get('supplier', null);
270
        $supplier = $supplier_id ? $em->find(Supplier::class, $supplier_id) : null;
271
        if (null !== $supplier && $new_part->getOrderdetails()->isEmpty()) {
272
            $orderdetail = new Orderdetail();
273
            $orderdetail->setSupplier($supplier);
274
            $new_part->addOrderdetail($orderdetail);
275
        }
276
277
        $form = $this->createForm(PartBaseType::class, $new_part);
278
279
        $form->handleRequest($request);
280
281
        if ($form->isSubmitted() && $form->isValid()) {
282
            //Upload passed files
283
            $attachments = $form['attachments'];
284
            foreach ($attachments as $attachment) {
285
                /** @var FormInterface $attachment */
286
                $options = [
287
                    'secure_attachment' => $attachment['secureFile']->getData(),
288
                    'download_url' => $attachment['downloadURL']->getData(),
289
                ];
290
291
                try {
292
                    $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
293
                } catch (AttachmentDownloadException $attachmentDownloadException) {
294
                    $this->addFlash(
295
                        'error',
296
                        $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
297
                    );
298
                }
299
            }
300
301
            $this->commentHelper->setMessage($form['log_comment']->getData());
302
303
            $em->persist($new_part);
304
            $em->flush();
305
            $this->addFlash('success', 'part.created_flash');
306
307
            //If a redirect URL was given, redirect there
308
            if ($request->query->get('_redirect')) {
309
                return $this->redirect($request->query->get('_redirect'));
310
            }
311
312
            //Redirect to clone page if user wished that...
313
            //@phpstan-ignore-next-line
314
            if ('save_and_clone' === $form->getClickedButton()->getName()) {
315
                return $this->redirectToRoute('part_clone', ['id' => $new_part->getID()]);
316
            }
317
            //@phpstan-ignore-next-line
318
            if ('save_and_new' === $form->getClickedButton()->getName()) {
319
                return $this->redirectToRoute('part_new');
320
            }
321
322
            return $this->redirectToRoute('part_edit', ['id' => $new_part->getID()]);
323
        }
324
325
        if ($form->isSubmitted() && !$form->isValid()) {
326
            $this->addFlash('error', 'part.created_flash.invalid');
327
        }
328
329
        return $this->renderForm('parts/edit/new_part.html.twig',
330
            [
331
                'part' => $new_part,
332
                'form' => $form,
333
            ]);
334
    }
335
336
    /**
337
     * @Route("/{id}/add_withdraw", name="part_add_withdraw", methods={"POST"})
338
     */
339
    public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
340
    {
341
        if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) {
342
            //Retrieve partlot from the request
343
            $partLot = $em->find(PartLot::class, $request->request->get('lot_id'));
344
            if($partLot === null) {
345
                throw new \RuntimeException('Part lot not found!');
346
            }
347
            //Ensure that the partlot belongs to the part
348
            if($partLot->getPart() !== $part) {
349
                throw new \RuntimeException("The origin partlot does not belong to the part!");
350
            }
351
            //Try to determine the target lot (used for move actions)
352
            $targetLot = $em->find(PartLot::class, $request->request->get('target_id'));
353
            if ($targetLot && $targetLot->getPart() !== $part) {
354
                throw new \RuntimeException("The target partlot does not belong to the part!");
355
            }
356
357
            //Extract the amount and comment from the request
358
            $amount = (float) $request->request->get('amount');
359
            $comment = $request->request->get('comment');
360
            $action = $request->request->get('action');
361
362
363
364
            switch ($action) {
365
                case "withdraw":
366
                case "remove":
367
                    $this->denyAccessUnlessGranted('withdraw', $partLot);
368
                    $withdrawAddHelper->withdraw($partLot, $amount, $comment);
369
                    break;
370
                case "add":
371
                    $this->denyAccessUnlessGranted('add', $partLot);
372
                    $withdrawAddHelper->add($partLot, $amount, $comment);
373
                    break;
374
                case "move":
375
                    $this->denyAccessUnlessGranted('move', $partLot);
376
                    $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment);
0 ignored issues
show
Bug introduced by
It seems like $targetLot can also be of type null; however, parameter $target of App\Services\Parts\PartL...thdrawAddHelper::move() does only seem to accept App\Entity\Parts\PartLot, maybe add an additional type check? ( Ignorable by Annotation )

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

376
                    $withdrawAddHelper->move($partLot, /** @scrutinizer ignore-type */ $targetLot, $amount, $comment);
Loading history...
377
                    break;
378
                default:
379
                    throw new \RuntimeException("Unknown action!");
380
            }
381
382
            //Save the changes to the DB
383
            $em->flush();
384
            $this->addFlash('success', 'part.withdraw.success');
385
386
        } else {
387
            $this->addFlash('error', 'CSRF Token invalid!');
388
        }
389
390
        //If an redirect was passed, then redirect there
391
        if($request->request->get('_redirect')) {
392
            return $this->redirect($request->request->get('_redirect'));
393
        }
394
        //Otherwise just redirect to the part page
395
        return $this->redirectToRoute('part_info', ['id' => $part->getID()]);
396
    }
397
}
398