Total Complexity | 52 |
Total Lines | 280 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like CourseRecycler 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 CourseRecycler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | final class CourseRecycler |
||
32 | { |
||
33 | public function __construct( |
||
34 | private readonly EntityManagerInterface $em, |
||
35 | private readonly string $courseCode, |
||
36 | private readonly int $courseId |
||
37 | ) {} |
||
38 | |||
39 | /** $type: 'full_backup' | 'select_items' ; $selected: [type => [id => true]] */ |
||
40 | public function recycle(string $type, array $selected): void |
||
41 | { |
||
42 | $isFull = ($type === 'full_backup'); |
||
43 | |||
44 | // If your EM doesn't have wrapInTransaction(), replace by $this->em->transactional(fn() => { ... }) |
||
45 | $this->em->wrapInTransaction(function () use ($isFull, $selected) { |
||
46 | // Links & categories |
||
47 | $this->recycleGeneric($isFull, CLink::class, $selected['link'] ?? []); |
||
48 | $this->recycleGeneric($isFull, CLinkCategory::class, $selected['link_category'] ?? [], autoClean: true); |
||
49 | |||
50 | // Calendar & announcements |
||
51 | $this->recycleGeneric($isFull, CCalendarEvent::class, $selected['event'] ?? []); |
||
52 | $this->recycleGeneric($isFull, CAnnouncement::class, $selected['announcement'] ?? []); |
||
53 | |||
54 | // Documents |
||
55 | $this->recycleGeneric($isFull, CDocument::class, $selected['document'] ?? [], deleteFiles: true); |
||
56 | |||
57 | // Forums & forum categories |
||
58 | $this->recycleGeneric($isFull, CForum::class, $selected['forum'] ?? [], cascadeHeavy: true); |
||
59 | $this->recycleGeneric($isFull, CForumCategory::class, $selected['forum_category'] ?? [], autoClean: true); |
||
60 | |||
61 | // Quizzes & categories |
||
62 | $this->recycleGeneric($isFull, CQuiz::class, $selected['quiz'] ?? [], cascadeHeavy: true); |
||
63 | $this->recycleGeneric($isFull, CQuizCategory::class, $selected['test_category'] ?? []); |
||
64 | |||
65 | // Surveys |
||
66 | $this->recycleGeneric($isFull, CSurvey::class, $selected['survey'] ?? [], cascadeHeavy: true); |
||
67 | |||
68 | // Learning paths & categories |
||
69 | $this->recycleGeneric($isFull, CLp::class, $selected['learnpath'] ?? [], cascadeHeavy: true, scormCleanup: true); |
||
70 | $this->recycleLpCategories($isFull, $selected['learnpath_category'] ?? []); |
||
71 | |||
72 | // Other resources |
||
73 | $this->recycleGeneric($isFull, CCourseDescription::class, $selected['course_description'] ?? []); |
||
74 | $this->recycleGeneric($isFull, CWiki::class, $selected['wiki'] ?? [], cascadeHeavy: true); |
||
75 | $this->recycleGeneric($isFull, CGlossary::class, $selected['glossary'] ?? []); |
||
76 | $this->recycleGeneric($isFull, CThematic::class, $selected['thematic'] ?? [], cascadeHeavy: true); |
||
77 | $this->recycleGeneric($isFull, CAttendance::class, $selected['attendance'] ?? [], cascadeHeavy: true); |
||
78 | $this->recycleGeneric($isFull, CStudentPublication::class, $selected['work'] ?? [], cascadeHeavy: true); |
||
79 | |||
80 | if ($isFull) { |
||
81 | // If you keep cleaning course picture: |
||
82 | // CourseManager::deleteCoursePicture($this->courseCode); |
||
83 | } |
||
84 | }); |
||
85 | } |
||
86 | |||
87 | /** |
||
88 | * Generic recycler for any AbstractResource-based entity. |
||
89 | * - If $isFull => deletes *all resources of that type* for the course. |
||
90 | * - If partial => deletes only the provided $ids. |
||
91 | * Options: |
||
92 | * - deleteFiles: physical files are already handled by hardDelete (if repo supports it). |
||
93 | * - cascadeHeavy: for heavy-relations types (forums, LPs). hardDelete should traverse. |
||
94 | * - autoClean: e.g. remove empty categories after deleting links/forums. |
||
95 | * - scormCleanup: if LP SCORM → hook storage service if needed. |
||
96 | */ |
||
97 | private function recycleGeneric( |
||
98 | bool $isFull, |
||
99 | string $entityClass, |
||
100 | array $idsMap, |
||
101 | bool $deleteFiles = false, |
||
102 | bool $cascadeHeavy = false, |
||
103 | bool $autoClean = false, |
||
104 | bool $scormCleanup = false |
||
105 | ): void { |
||
106 | if ($isFull) { |
||
107 | $this->deleteAllOfTypeForCourse($entityClass); |
||
108 | if ($autoClean) { |
||
109 | $this->autoCleanIfSupported($entityClass); |
||
110 | } |
||
111 | if ($scormCleanup && $entityClass === CLp::class) { |
||
112 | $this->cleanupScormDirsForAllLp(); |
||
113 | } |
||
114 | return; |
||
115 | } |
||
116 | |||
117 | $ids = $this->ids($idsMap); |
||
118 | if (!$ids) { |
||
|
|||
119 | if ($autoClean) { |
||
120 | $this->autoCleanIfSupported($entityClass); |
||
121 | } |
||
122 | return; |
||
123 | } |
||
124 | |||
125 | $this->deleteSelectedOfTypeForCourse($entityClass, $ids); |
||
126 | |||
127 | if ($autoClean) { |
||
128 | $this->autoCleanIfSupported($entityClass); |
||
129 | } |
||
130 | if ($scormCleanup && $entityClass === CLp::class) { |
||
131 | $this->cleanupScormDirsForLpIds($ids); |
||
132 | } |
||
133 | } |
||
134 | |||
135 | /** LP categories: detach LPs and then delete selected/all categories */ |
||
136 | private function recycleLpCategories(bool $isFull, array $idsMap): void |
||
137 | { |
||
138 | if ($isFull) { |
||
139 | // Detach all categories from LPs in course |
||
140 | $this->clearLpCategoriesForCourse(); |
||
141 | $this->deleteAllOfTypeForCourse(CLpCategory::class); |
||
142 | return; |
||
143 | } |
||
144 | |||
145 | $ids = $this->ids($idsMap); |
||
146 | if (!$ids) { |
||
147 | return; |
||
148 | } |
||
149 | |||
150 | // Detach LPs from these categories |
||
151 | $this->clearLpCategoriesForIds($ids); |
||
152 | $this->deleteSelectedOfTypeForCourse(CLpCategory::class, $ids); |
||
153 | } |
||
154 | |||
155 | /** Normalizes IDs from [id => true] maps into int/string scalars */ |
||
156 | private function ids(array $map): array |
||
157 | { |
||
158 | return array_values(array_filter(array_map( |
||
159 | static fn($k) => is_numeric($k) ? (int) $k : (string) $k, |
||
160 | array_keys($map) |
||
161 | ), static fn($v) => $v !== '' && $v !== null)); |
||
162 | } |
||
163 | |||
164 | /** Lightweight Course reference for query builders */ |
||
165 | private function courseRef(): CoreCourse |
||
170 | } |
||
171 | |||
172 | /** |
||
173 | * Fetches resources by entity class within course, optionally filtering by resource iid. |
||
174 | * If the repository doesn't extend ResourceRepository, falls back to a generic QB. |
||
175 | * |
||
176 | * @return array<int, AbstractResource> |
||
177 | */ |
||
178 | private function fetchResourcesForCourse(string $entityClass, ?array $ids = null): array |
||
179 | { |
||
180 | $repo = $this->em->getRepository($entityClass); |
||
181 | |||
182 | // Path A: repository exposes ResourceRepository API |
||
183 | if (method_exists($repo, 'getResourcesByCourseIgnoreVisibility')) { |
||
184 | $qb = $repo->getResourcesByCourseIgnoreVisibility($this->courseRef()); |
||
185 | if ($ids && \count($ids) > 0) { |
||
186 | $qb->andWhere('resource.iid IN (:ids)')->setParameter('ids', $ids); |
||
187 | } |
||
188 | return $qb->getQuery()->getResult(); |
||
189 | } |
||
190 | |||
191 | // Path B: generic fallback (join to ResourceNode/ResourceLinks and filter by course) |
||
192 | $qb = $this->em->createQueryBuilder() |
||
193 | ->select('resource') |
||
194 | ->from($entityClass, 'resource') |
||
195 | ->innerJoin('resource.resourceNode', 'node') |
||
196 | ->innerJoin('node.resourceLinks', 'links') |
||
197 | ->andWhere('links.course = :course') |
||
198 | ->setParameter('course', $this->courseRef()); |
||
199 | |||
200 | if ($ids && \count($ids) > 0) { |
||
201 | $qb->andWhere('resource.iid IN (:ids)')->setParameter('ids', $ids); |
||
202 | } |
||
203 | |||
204 | return $qb->getQuery()->getResult(); |
||
205 | } |
||
206 | |||
207 | /** |
||
208 | * Hard-deletes a list of resources. If repository doesn't provide hardDelete(), |
||
209 | * falls back to EM->remove() and a final flush (expect proper cascade mappings). |
||
210 | */ |
||
211 | private function hardDeleteMany(string $entityClass, array $resources): void |
||
230 | } |
||
231 | } |
||
232 | |||
233 | /** Deletes all resources of a type in the course */ |
||
234 | private function deleteAllOfTypeForCourse(string $entityClass): void |
||
235 | { |
||
236 | $resources = $this->fetchResourcesForCourse($entityClass, null); |
||
237 | if ($resources) { |
||
238 | $this->hardDeleteMany($entityClass, $resources); |
||
239 | } |
||
240 | } |
||
241 | |||
242 | /** Deletes selected resources (by iid) of a type in the course */ |
||
243 | private function deleteSelectedOfTypeForCourse(string $entityClass, array $ids): void |
||
244 | { |
||
245 | if (!$ids) { |
||
246 | return; |
||
247 | } |
||
248 | $resources = $this->fetchResourcesForCourse($entityClass, $ids); |
||
249 | if ($resources) { |
||
250 | $this->hardDeleteMany($entityClass, $resources); |
||
251 | } |
||
252 | } |
||
253 | |||
254 | /** Optional post-clean for empty categories if repository supports it */ |
||
255 | private function autoCleanIfSupported(string $entityClass): void |
||
260 | } |
||
261 | } |
||
262 | |||
263 | /** Detach categories from ALL LPs in course (repo-level bulk method preferred if available) */ |
||
264 | private function clearLpCategoriesForCourse(): void |
||
265 | { |
||
266 | $lps = $this->fetchResourcesForCourse(CLp::class, null); |
||
267 | $changed = false; |
||
268 | foreach ($lps as $lp) { |
||
269 | if (method_exists($lp, 'getCategory') && method_exists($lp, 'setCategory')) { |
||
270 | if ($lp->getCategory()) { |
||
271 | $lp->setCategory(null); |
||
272 | $this->em->persist($lp); |
||
273 | $changed = true; |
||
274 | } |
||
275 | } |
||
276 | } |
||
277 | if ($changed) { |
||
278 | $this->em->flush(); |
||
279 | } |
||
280 | } |
||
281 | |||
282 | /** Detach categories only for LPs that are linked to given category ids */ |
||
283 | private function clearLpCategoriesForIds(array $catIds): void |
||
284 | { |
||
285 | $lps = $this->fetchResourcesForCourse(CLp::class, null); |
||
286 | $changed = false; |
||
287 | foreach ($lps as $lp) { |
||
288 | $cat = method_exists($lp, 'getCategory') ? $lp->getCategory() : null; |
||
289 | $catId = $cat?->getId(); |
||
290 | if ($catId !== null && \in_array($catId, $catIds, true) && method_exists($lp, 'setCategory')) { |
||
291 | $lp->setCategory(null); |
||
292 | $this->em->persist($lp); |
||
293 | $changed = true; |
||
294 | } |
||
295 | } |
||
296 | if ($changed) { |
||
297 | $this->em->flush(); |
||
298 | } |
||
299 | } |
||
300 | |||
301 | /** SCORM directory cleanup for ALL LPs (hook your storage service here if needed) */ |
||
302 | private function cleanupScormDirsForAllLp(): void |
||
304 | // If you have a storage/scorm service, invoke it here. |
||
305 | // By default, nothing: hardDelete already deletes files linked to ResourceNode. |
||
306 | } |
||
307 | |||
308 | /** SCORM directory cleanup for selected LPs */ |
||
309 | private function cleanupScormDirsForLpIds(array $lpIds): void |
||
310 | { |
||
311 | // Same as above, but limited to provided LP ids. |
||
312 | } |
||
313 | } |
||
314 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.