AppointmentController   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 269
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 40
eloc 159
c 0
b 0
f 0
dl 0
loc 269
rs 9.2
ccs 0
cts 144
cp 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getMessageScope() 0 2 1
F indexXhr() 0 207 34
A index() 0 21 1
A ics() 0 7 1
A export() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like AppointmentController 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 AppointmentController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Controller;
4
5
use Symfony\Component\HttpFoundation\Response;
6
use App\Converter\StudyGroupsGradeStringConverter;
7
use App\Converter\TeacherStringConverter;
8
use App\Converter\UserStringConverter;
9
use App\Entity\Appointment;
10
use App\Entity\AppointmentCategory;
11
use App\Entity\IcsAccessToken;
12
use App\Entity\IcsAccessTokenType;
13
use App\Entity\Exam;
14
use App\Entity\Grade;
15
use App\Entity\MessageScope;
16
use App\Entity\Teacher;
17
use App\Entity\Tuition;
18
use App\Entity\User;
19
use App\Entity\UserType;
20
use App\Export\AppointmentIcsExporter;
21
use App\Form\IcsAccessTokenType as DeviceTokenTypeForm;
22
use App\Repository\AppointmentRepositoryInterface;
23
use App\Repository\ExamRepositoryInterface;
24
use App\Repository\ImportDateTypeRepositoryInterface;
25
use App\Security\IcsAccessToken\IcsAccessTokenManager;
26
use App\Security\Voter\AppointmentVoter;
27
use App\Security\Voter\ExamVoter;
28
use App\Settings\AppointmentsSettings;
29
use App\Settings\TimetableSettings;
30
use App\Timetable\TimetableTimeHelper;
31
use App\Utils\ArrayUtils;
32
use App\Utils\ColorUtils;
33
use App\View\Filter\AppointmentCategoriesFilter;
34
use App\View\Filter\GradesFilter;
35
use App\View\Filter\SectionFilter;
36
use App\View\Filter\StudentFilter;
37
use App\View\Filter\StudyGroupFilter;
38
use App\View\Filter\TeacherFilter;
39
use DateInterval;
40
use DateTime;
41
use Symfony\Component\HttpFoundation\Request;
42
use Symfony\Component\Routing\Annotation\Route;
43
use Symfony\Contracts\Translation\TranslatorInterface;
44
45
#[Route(path: '/appointments')]
46
class AppointmentController extends AbstractControllerWithMessages {
47
48
    #[Route(path: '', name: 'appointments')]
49
    public function index(SectionFilter $sectionFilter, AppointmentCategoriesFilter $categoryFilter, StudentFilter $studentFilter, StudyGroupFilter $studyGroupFilter,
50
                          TeacherFilter $teacherFilter, GradesFilter $gradesFilter, Request $request, ImportDateTypeRepositoryInterface $importDateTypeRepository): Response {
51
        /** @var User $user */
52
        $user = $this->getUser();
53
54
        $categoryFilterView = $categoryFilter->handle([ ]);
55
        $sectionFilterView = $sectionFilter->handle($request->query->get('section'));
56
        $studentFilterView = $studentFilter->handle(null, $sectionFilterView->getCurrentSection(), $user);
57
        $studyGroupView = $studyGroupFilter->handle(null, $sectionFilterView->getCurrentSection(), $user);
58
        $teacherFilterView = $teacherFilter->handle(null, $sectionFilterView->getCurrentSection(), $user, false);
59
        $gradesFilterView = $gradesFilter->handle([], $sectionFilterView->getCurrentSection(), $user);
60
61
        return $this->renderWithMessages('appointments/index.html.twig', [
62
            'sectionFilter' => $sectionFilterView,
63
            'categoryFilter' => $categoryFilterView,
64
            'studentFilter' => $studentFilterView,
65
            'studyGroupFilter' => $studyGroupView,
66
            'teacherFilter' => $teacherFilterView,
67
            'examGradesFilter' => $gradesFilterView,
68
            'last_import' => $importDateTypeRepository->findOneByEntityClass(Appointment::class)
69
        ]);
70
    }
71
72
    #[Route(path: '/xhr', name: 'appointments_xhr', methods: ['GET'])]
73
    public function indexXhr(AppointmentRepositoryInterface $appointmentRepository, ColorUtils $colorUtils, TranslatorInterface $translator,
74
                             StudyGroupsGradeStringConverter $studyGroupsGradeStringConverter, TeacherStringConverter $teacherStringConverter,
75
                             AppointmentCategoriesFilter $categoryFilter, SectionFilter $sectionFilter, StudentFilter $studentFilter, StudyGroupFilter $studyGroupFilter,
76
                             TeacherFilter $teacherFilter, GradesFilter $gradesFilter, ExamRepositoryInterface $examRepository,
77
                             AppointmentsSettings $appointmentsSettings, TimetableTimeHelper $timetableTimeHelper, UserStringConverter $userStringConverter, Request $request): Response {
78
        /** @var User $user */
79
        $user = $this->getUser();
80
        $isStudent = $user->isStudent();
81
        $isParent = $user->isParent();
82
83
        $sectionFilterView = $sectionFilter->handle($request->query->get('section'));
84
        $showAll = $request->query->getBoolean('all', false);
0 ignored issues
show
Unused Code introduced by
The assignment to $showAll is dead and can be removed.
Loading history...
85
        $categoryFilterView = $categoryFilter->handle(explode(',', $request->query->get('categories', '')));
86
        $studentFilterView = $studentFilter->handle($request->query->get('student', null), $sectionFilterView->getCurrentSection(), $user);
87
        $studyGroupView = $studyGroupFilter->handle($request->query->get('study_group', null), $sectionFilterView->getCurrentSection(), $user);
88
        $teacherFilterView = $teacherFilter->handle($request->query->get('teacher', null), $sectionFilterView->getCurrentSection(), $user, $studentFilterView->getCurrentStudent() === null && $studyGroupView->getCurrentStudyGroup() === null);
89
        $examGradesFilterView = $gradesFilter->handle(explode(',', $request->query->get('exam_grades', '')), $sectionFilterView->getCurrentSection(), $user);
90
91
        $appointments = [ ];
92
        $today = null;
93
94
        if($studentFilterView->getCurrentStudent() !== null) {
95
            $appointments = $appointmentRepository->findAllForStudents([$studentFilterView->getCurrentStudent()], $today);
96
        } else if($studyGroupView->getCurrentStudyGroup() !== null) {
97
            $appointments = $appointmentRepository->findAllForStudyGroup($studyGroupView->getCurrentStudyGroup(), $today);
98
        } else if($teacherFilterView->getCurrentTeacher() !== null) {
99
            $appointments = $appointmentRepository->findAllForTeacher($teacherFilterView->getCurrentTeacher(), $today);
100
        } else {
101
            if($isStudent || $isParent) {
102
                $appointments = $appointmentRepository->findAllForStudents($user->getStudents()->toArray(), $today);
103
            } else {
104
                $appointments = $appointmentRepository->findAll([ ], null, $today);
105
            }
106
        }
107
108
        if(!empty($categoryFilterView->getCurrentCategories())) {
109
            $selectedCategoryIds = array_map(fn(AppointmentCategory $category) => $category->getId(), $categoryFilterView->getCurrentCategories());
110
111
            $appointments = array_filter($appointments, fn(Appointment $appointment) => in_array($appointment->getCategory()->getId(), $selectedCategoryIds));
112
        }
113
114
        $json = [ ];
115
116
        foreach($appointments as $appointment) {
117
            if($this->isGranted(AppointmentVoter::View, $appointment) !== true) {
118
                continue;
119
            }
120
121
            if($appointment->isAllDay() && $appointment->getDuration()->d === 1) {
0 ignored issues
show
Bug introduced by
The method isAllDay() does not exist on null. ( Ignorable by Annotation )

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

121
            if($appointment->/** @scrutinizer ignore-call */ isAllDay() && $appointment->getDuration()->d === 1) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
122
                $view = [
123
                    [
124
                        'label' => $translator->trans('label.date'),
125
                        'content' => $appointment->getStart()->format($translator->trans('date.format'))
126
                    ]
127
                ];
128
            } else {
129
                $view = [
130
                    [
131
                        'label' => $translator->trans('label.start'),
132
                        'content' => $appointment->getStart()->format($translator->trans($appointment->isAllDay() ? 'date.format' : 'date.with_time'))
133
                    ],
134
                    [
135
                        'label' => $translator->trans('label.end'),
136
                        'content' => $appointment->getRealEnd()->format($translator->trans($appointment->isAllDay() ? 'date.format' : 'date.with_time'))
137
                    ]
138
                ];
139
            }
140
141
            if(!empty($appointment->getLocation())) {
142
                $view[] = [
143
                    'label' => $translator->trans('label.location'),
144
                    'content' => $appointment->getLocation()
145
                ];
146
            }
147
148
            if($appointment->getStudyGroups()->count() > 0) {
149
                $view[] = [
150
                    'label' => $translator->trans('label.study_groups', ['%count%' => $appointment->getStudyGroups()->count()]),
151
                    'content' => $studyGroupsGradeStringConverter->convert($appointment->getStudyGroups())
152
                ];
153
            }
154
155
            if($appointment->getOrganizers()->count() > 0) {
156
                $view[] = [
157
                    'label' => $translator->trans('label.organizers'),
158
                    'content' => implode(', ', array_map(fn(Teacher $teacher) => $teacherStringConverter->convert($teacher), $appointment->getOrganizers()->toArray()))
159
                ];
160
            }
161
162
            if(!empty($appointment->getExternalOrganizers())) {
163
                $view[] = [
164
                    'label' => $translator->trans('label.external_organizers'),
165
                    'content' => $appointment->getExternalOrganizers()
166
                ];
167
            }
168
169
            if($appointment->getCreatedBy() !== null) {
170
                $view[] = [
171
                    'label' => $translator->trans('label.created_by'),
172
                    'content' => $userStringConverter->convert($appointment->getCreatedBy(), false)
173
                ];
174
            }
175
176
            $json[] = [
177
                'uuid' => $appointment->getUuid(),
178
                'allDay' => $appointment->isAllDay(),
179
                'title' => ($appointment->isConfirmed() === false ? '(✗) ' : '') . $appointment->getTitle(),
180
                'textColor' => $colorUtils->getForeground($appointment->getCategory()->getColor()),
181
                'backgroundColor' => $appointment->getCategory()->getColor(),
182
                'start' => $appointment->getStart()->format($appointment->isAllDay() ? 'Y-m-d' : 'Y-m-d H:i'),
183
                'end' => $appointment->getEnd()->format($appointment->isAllDay() ? 'Y-m-d' : 'Y-m-d H:i'),
184
                'extendedProps' => [
185
                    'category'=> $appointment->getCategory()->getName(),
186
                    'content' => $appointment->getContent(),
187
                    'view' => $view,
188
                    'confirmation_status' => $appointment->isConfirmed() === false ? $translator->trans('label.not_confirmed') : null
189
                ]
190
            ];
191
        }
192
193
        // Add exams
194
        if((is_countable($examGradesFilterView->getCurrentGrades()) ? count($examGradesFilterView->getCurrentGrades()) : 0) > 0) {
195
            $exams = [ ];
196
197
            foreach($examGradesFilterView->getCurrentGrades() as $grade) {
198
                $exams = array_merge($exams, $examRepository->findAllByGrade($grade));
199
            }
200
201
            $exams = ArrayUtils::createArrayWithKeys($exams, fn(Exam $exam) => $exam->getUuid()->toString());
202
203
            /** @var Exam $exam */
204
            foreach($exams as $exam) {
205
                if($this->isGranted(ExamVoter::Show, $exam)) {
206
                    $view = [ ];
207
208
                    $tuitions = implode(', ', $exam->getTuitions()->map(fn(Tuition $tuition) => $tuition->getName())->toArray());
0 ignored issues
show
Bug introduced by
The method getTuitions() does not exist on null. ( Ignorable by Annotation )

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

208
                    $tuitions = implode(', ', $exam->/** @scrutinizer ignore-call */ getTuitions()->map(fn(Tuition $tuition) => $tuition->getName())->toArray());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
209
210
                    $view[] = [
211
                        'label' => $translator->trans('label.tuitions'),
212
                        'content' => $tuitions
213
                    ];
214
215
                    $grades = [ ];
216
                    $teachers = [ ];
217
218
                    /** @var Tuition $tuition */
219
                    foreach($exam->getTuitions() as $tuition) {
220
                        /** @var Grade $grade */
221
                        foreach($tuition->getStudyGroup()->getGrades() as $grade) {
222
                            $grades[] = $grade->getName();
223
                        }
224
225
                        /** @var Teacher $teacher */
226
                        foreach($tuition->getTeachers() as $teacher) {
227
                            $teachers[] = $teacherStringConverter->convert($teacher);
228
                        }
229
230
                        $grades = array_unique($grades);
231
                        $teachers = array_unique($teachers);
232
                    }
233
234
                    $view[] = [
235
                        'label' => $translator->trans('label.teacher'),
236
                        'content' => $teachers
237
                    ];
238
239
                    $view[] = [
240
                        'label' => $translator->trans('label.grades'),
241
                        'content' => implode(', ', $grades)
242
                    ];
243
244
                    if($exam->getRoom() !== null) {
245
                        $view[] = [
246
                            'label' => $translator->trans('label.room'),
247
                            'content' => $exam->getRoom()->getName()
248
                        ];
249
                    }
250
251
                    $view[] = [
252
                        'label' => $translator->trans('plans.exams.time'),
253
                        'content' => $translator->trans('label.exam_lessons', [
254
                            '%start%' => $exam->getLessonStart(),
255
                            '%end%' => $exam->getLessonEnd(),
256
                            '%count%' => $exam->getLessonEnd() - $exam->getLessonStart()
257
                        ])
258
                    ];
259
260
                    $json[] = [
261
                        'uuid' => $exam->getUuid(),
262
                        'allDay' => false,
263
                        'title' => sprintf('%s: %s', implode(', ', $grades), $tuitions),
264
                        'textColor' => empty($appointmentsSettings->getExamColor()) ? '#000000' : $colorUtils->getForeground($appointmentsSettings->getExamColor()),
265
                        'backgroundColor' => empty($appointmentsSettings->getExamColor()) ? '#ffffff' : $appointmentsSettings->getExamColor(),
266
                        'start' => $timetableTimeHelper->getLessonStartDateTime($exam->getDate(), $exam->getLessonStart())->format('Y-m-d H:i'),
267
                        'end' => $timetableTimeHelper->getLessonEndDateTime($exam->getDate(), $exam->getLessonEnd())->format('Y-m-d H:i'),
268
                        'extendedProps' => [
269
                            'category' => $translator->trans('plans.exams.label'),
270
                            'content' => $exam->getDescription(),
271
                            'view' => $view
272
                        ]
273
                    ];
274
                }
275
            }
276
        }
277
278
        return $this->json($json);
279
    }
280
281
    #[Route(path: '/export', name: 'appointments_export')]
282
    public function export(Request $request, IcsAccessTokenManager $manager): Response {
283
        /** @var User $user */
284
        $user = $this->getUser();
285
286
        $deviceToken = (new IcsAccessToken())
287
            ->setType(IcsAccessTokenType::Calendar)
288
            ->setUser($user);
289
290
        $form = $this->createForm(DeviceTokenTypeForm::class, $deviceToken);
291
        $form->handleRequest($request);
292
293
        if($form->isSubmitted() && $form->isValid()) {
294
            $deviceToken = $manager->persistToken($deviceToken);
295
        }
296
297
        return $this->renderWithMessages('appointments/export.html.twig', [
298
            'form' => $form->createView(),
299
            'token' => $deviceToken
300
        ]);
301
    }
302
303
    #[Route(path: '/ics/download', name: 'appointments_ics')]
304
    #[Route(path: '/ics/download/{token}', name: 'appointments_ics_token')]
305
    public function ics(AppointmentIcsExporter $exporter): Response {
306
        /** @var User $user */
307
        $user = $this->getUser();
308
309
        return $exporter->getIcsResponse($user);
310
    }
311
312
    protected function getMessageScope(): MessageScope {
313
        return MessageScope::Appointments;
314
    }
315
}