| Total Complexity | 58 |
| Total Lines | 304 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 1 |
Complex classes like GradebookMetaExport 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 GradebookMetaExport, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 19 | class GradebookMetaExport extends ActivityExport |
||
| 20 | { |
||
| 21 | /** |
||
| 22 | * Export one Gradebook snapshot as JSON + manifest entry. |
||
| 23 | * |
||
| 24 | * @param int $activityId Legacy/opaque wrapper id (not strictly required) |
||
| 25 | * @param string $exportDir Absolute temp export directory (root of backup) |
||
| 26 | * @param int $moduleId Synthetic module id used to name files |
||
| 27 | * @param int $sectionId Section (topic) id; informative for manifest |
||
| 28 | */ |
||
| 29 | public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void |
||
| 30 | { |
||
| 31 | $backup = $this->findGradebookBackup($activityId); |
||
| 32 | if ($backup === null) { |
||
| 33 | @error_log('[GradebookMetaExport] Skip: gradebook backup not found for activityId=' . $activityId); |
||
|
|
|||
| 34 | return; |
||
| 35 | } |
||
| 36 | |||
| 37 | $payload = $this->buildPayloadFromBackup($backup, $moduleId, $sectionId); |
||
| 38 | |||
| 39 | // Ensure base dir exists: {exportDir}/chamilo/gradebook |
||
| 40 | $base = rtrim($exportDir, '/') . '/chamilo/gradebook'; |
||
| 41 | if (!\is_dir($base)) { |
||
| 42 | @mkdir($base, (int)\octdec('0775'), true); |
||
| 43 | } |
||
| 44 | |||
| 45 | // Write JSON: chamilo/gradebook/gradebook_{moduleId}.json |
||
| 46 | $jsonFile = $base . '/gradebook_' . $moduleId . '.json'; |
||
| 47 | @file_put_contents( |
||
| 48 | $jsonFile, |
||
| 49 | \json_encode($payload, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT) |
||
| 50 | ); |
||
| 51 | |||
| 52 | // Append entry to chamilo/manifest.json |
||
| 53 | $this->appendToManifest($exportDir, [ |
||
| 54 | 'kind' => 'gradebook', |
||
| 55 | 'moduleid' => $moduleId, |
||
| 56 | 'sectionid' => $sectionId, |
||
| 57 | 'title' => 'Gradebook', |
||
| 58 | 'path' => 'chamilo/gradebook/gradebook_' . $moduleId . '.json', |
||
| 59 | ]); |
||
| 60 | |||
| 61 | @error_log('[GradebookMetaExport] Exported gradebook meta moduleId=' . $moduleId . ' sectionId=' . $sectionId); |
||
| 62 | } |
||
| 63 | |||
| 64 | /** |
||
| 65 | * Locate the GradeBookBackup wrapper from the CourseBuilder bag. |
||
| 66 | * Robust rules: |
||
| 67 | * - Accept constant or string keys ("gradebook", "Gradebook"). |
||
| 68 | * - If there is only ONE entry, return its first value without assuming index 0. |
||
| 69 | * - Otherwise, loosely match by "source_id" or "id" against $iid (string tolerant). |
||
| 70 | * - Finally, return the first object that exposes a "categories" array. |
||
| 71 | */ |
||
| 72 | private function findGradebookBackup(int $iid): ?object |
||
| 123 | } |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Build JSON payload from GradeBookBackup wrapper. |
||
| 127 | * The wrapper already contains the serialized array produced by the builder. |
||
| 128 | * We pass it through, applying minimal normalization. |
||
| 129 | * |
||
| 130 | * Additionally, we compute a best-effort list of "assessed_refs" pointing to |
||
| 131 | * referenced activities (e.g., quiz/assign ids) if such hints are present in the categories. |
||
| 132 | */ |
||
| 133 | private function buildPayloadFromBackup(object $backup, int $moduleId, int $sectionId): array |
||
| 134 | { |
||
| 135 | $categories = $this->readCategories($backup); |
||
| 136 | $assessed = $this->computeAssessedRefsFromCategories($categories); |
||
| 137 | |||
| 138 | return [ |
||
| 139 | 'type' => 'gradebook', |
||
| 140 | 'moduleid' => $moduleId, |
||
| 141 | 'sectionid' => $sectionId, |
||
| 142 | 'title' => 'Gradebook', |
||
| 143 | 'categories' => $categories, // structure as produced by serializeGradebookCategory() |
||
| 144 | 'assessed_refs' => $assessed, // best-effort references for Chamilo re-import |
||
| 145 | '_exportedAt' => \date('c'), |
||
| 146 | ]; |
||
| 147 | } |
||
| 148 | |||
| 149 | /** |
||
| 150 | * Read and normalize categories from the wrapper. |
||
| 151 | * Accepts arrays, Traversables and shallow objects of arrays. |
||
| 152 | */ |
||
| 153 | private function readCategories(object $backup): array |
||
| 154 | { |
||
| 155 | // Direct property first |
||
| 156 | if (isset($backup->categories)) { |
||
| 157 | return $this->deepArray($backup->categories); |
||
| 158 | } |
||
| 159 | |||
| 160 | // Common getters |
||
| 161 | foreach (['getCategories', 'get_categories'] as $m) { |
||
| 162 | if (\is_callable([$backup, $m])) { |
||
| 163 | try { |
||
| 164 | $v = $backup->{$m}(); |
||
| 165 | return $this->deepArray($v); |
||
| 166 | } catch (\Throwable) { |
||
| 167 | // ignore and continue |
||
| 168 | } |
||
| 169 | } |
||
| 170 | } |
||
| 171 | |||
| 172 | // Nothing found |
||
| 173 | return []; |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * Convert input into a JSON-safe array recursively. |
||
| 178 | * - Arrays are copied deeply |
||
| 179 | * - Traversables become arrays |
||
| 180 | * - StdClass/DTOs with public props are cast to (array) and normalized |
||
| 181 | * Note: we intentionally DO NOT traverse Doctrine entities here; the builder already serialized them. |
||
| 182 | */ |
||
| 183 | private function deepArray(mixed $value): array |
||
| 184 | { |
||
| 185 | if (\is_array($value)) { |
||
| 186 | $out = []; |
||
| 187 | foreach ($value as $k => $v) { |
||
| 188 | // inner values may be arrays or scalars; recurse only for arrays/objects/traversables |
||
| 189 | if (\is_array($v) || $v instanceof \Traversable || \is_object($v)) { |
||
| 190 | $out[$k] = $this->deepArray($v); |
||
| 191 | } else { |
||
| 192 | $out[$k] = $v; |
||
| 193 | } |
||
| 194 | } |
||
| 195 | return $out; |
||
| 196 | } |
||
| 197 | |||
| 198 | if ($value instanceof \Traversable) { |
||
| 199 | return $this->deepArray(\iterator_to_array($value)); |
||
| 200 | } |
||
| 201 | |||
| 202 | if (\is_object($value)) { |
||
| 203 | // Cast public properties, then normalize |
||
| 204 | return $this->deepArray((array) $value); |
||
| 205 | } |
||
| 206 | |||
| 207 | // If a scalar reaches here at the top-level, normalize to array |
||
| 208 | return [$value]; |
||
| 209 | } |
||
| 210 | |||
| 211 | /** |
||
| 212 | * Attempt to derive a minimal set of references to assessed activities |
||
| 213 | * from the categories structure. This is *best-effort* and will only |
||
| 214 | * collect what is already serialized by the builder. |
||
| 215 | * |
||
| 216 | * Output example: |
||
| 217 | * [ |
||
| 218 | * {"type":"quiz","id":123}, |
||
| 219 | * {"type":"assign","id":45} |
||
| 220 | * ] |
||
| 221 | */ |
||
| 222 | private function computeAssessedRefsFromCategories(array $categories): array |
||
| 287 | } |
||
| 288 | |||
| 289 | /** |
||
| 290 | * Append record to chamilo/manifest.json (create if missing). |
||
| 291 | */ |
||
| 292 | private function appendToManifest(string $exportDir, array $record): void |
||
| 323 | ); |
||
| 324 | } |
||
| 326 |
If you suppress an error, we recommend checking for the error condition explicitly: