Passed
Pull Request — master (#6896)
by
unknown
08:56
created

CDocumentRepository   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 473
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 1
Metric Value
eloc 218
c 4
b 1
f 1
dl 0
loc 473
rs 8.5599
wmc 48

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getFolderSize() 0 3 1
A countUserDocuments() 0 7 1
A createFileInFolder() 0 39 4
A registerScormZip() 0 28 1
A __construct() 0 3 1
A ensureFolder() 0 41 5
A getCourseDocumentsRootNode() 0 45 3
A purgeScormZip() 0 20 5
A getParent() 0 9 2
A findChildNodeByTitle() 0 7 1
A ensureCourseDocumentsRootNode() 0 34 4
A tryDeleteFirstFolderByTitlePrefix() 0 21 2
A addFileTypeQueryBuilder() 0 7 1
A findDocumentsByAuthor() 0 11 1
A safeTitle() 0 6 2
B getAllDocumentDataByUserAndGroup() 0 74 7
A em() 0 4 1
A ensureLearningPathSystemFolder() 0 28 6

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Repository;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceLink;
11
use Chamilo\CoreBundle\Entity\ResourceNode;
12
use Chamilo\CoreBundle\Entity\Session;
13
use Chamilo\CoreBundle\Entity\User;
14
use Chamilo\CoreBundle\Repository\ResourceRepository;
15
use Chamilo\CourseBundle\Entity\CDocument;
16
use Chamilo\CourseBundle\Entity\CGroup;
17
use Chamilo\CourseBundle\Entity\CLp;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Doctrine\ORM\QueryBuilder;
20
use Doctrine\Persistence\ManagerRegistry;
21
use RuntimeException;
22
use Symfony\Component\HttpFoundation\File\UploadedFile;
23
24
final class CDocumentRepository extends ResourceRepository
25
{
26
    public function __construct(ManagerRegistry $registry)
27
    {
28
        parent::__construct($registry, CDocument::class);
29
    }
30
31
    public function getParent(CDocument $document): ?CDocument
32
    {
33
        $resourceParent = $document->getResourceNode()->getParent();
34
35
        if (null !== $resourceParent) {
36
            return $this->findOneBy(['resourceNode' => $resourceParent->getId()]);
37
        }
38
39
        return null;
40
    }
41
42
    public function getFolderSize(ResourceNode $resourceNode, Course $course, ?Session $session = null): int
43
    {
44
        return $this->getResourceNodeRepository()->getSize($resourceNode, $this->getResourceType(), $course, $session);
45
    }
46
47
    /**
48
     * @return CDocument[]
49
     */
50
    public function findDocumentsByAuthor(int $userId)
51
    {
52
        $qb = $this->createQueryBuilder('d');
53
54
        return $qb
55
            ->innerJoin('d.resourceNode', 'node')
56
            ->innerJoin('node.resourceLinks', 'l')
57
            ->where('l.user = :user')
58
            ->setParameter('user', $userId)
59
            ->getQuery()
60
            ->getResult()
61
        ;
62
    }
63
64
    public function countUserDocuments(User $user, Course $course, ?Session $session = null, ?CGroup $group = null): int
65
    {
66
        $qb = $this->getResourcesByCourseLinkedToUser($user, $course, $session, $group);
67
        $qb->select('count(resource)');
68
        $this->addFileTypeQueryBuilder('file', $qb);
69
70
        return $this->getCount($qb);
71
    }
72
73
    protected function addFileTypeQueryBuilder(string $fileType, ?QueryBuilder $qb = null): QueryBuilder
74
    {
75
        $qb = $this->getOrCreateQueryBuilder($qb);
76
77
        return $qb
78
            ->andWhere('resource.filetype = :filetype')
79
            ->setParameter('filetype', $fileType)
80
        ;
81
    }
82
83
    /**
84
     * Register the original SCORM ZIP under:
85
     *   <course root> / Learning paths / SCORM - {lp_id} - {lp_title} / {lp_title}.zip
86
     */
87
    public function registerScormZip(Course $course, ?Session $session, CLp $lp, UploadedFile $zip): void
88
    {
89
        $em = $this->em();
90
91
        // Ensure "Learning paths" directly under the course resource node
92
        $lpTop = $this->ensureLearningPathSystemFolder($course, $session);
93
94
        // Subfolder per LP
95
        $lpFolderTitle = \sprintf('SCORM - %d - %s', $lp->getIid(), $this->safeTitle($lp->getTitle()));
96
        $lpFolder = $this->ensureFolder(
97
            $course,
98
            $lpTop,
99
            $lpFolderTitle,
100
            ResourceLink::VISIBILITY_DRAFT,
101
            $session
102
        );
103
104
        // ZIP file under the LP folder
105
        $this->createFileInFolder(
106
            $course,
107
            $lpFolder,
108
            $zip,
109
            \sprintf('SCORM ZIP for LP #%d', $lp->getIid()),
110
            ResourceLink::VISIBILITY_DRAFT,
111
            $session
112
        );
113
114
        $em->flush();
115
    }
116
117
    /**
118
     * Remove the LP subfolder "SCORM - {lp_id} - ..." under "Learning paths".
119
     */
120
    public function purgeScormZip(Course $course, CLp $lp): void
121
    {
122
        $em = $this->em();
123
        $prefix = \sprintf('SCORM - %d - ', $lp->getIid());
124
125
        $courseRoot = $course->getResourceNode();
126
        if ($courseRoot) {
127
            // SCORM folder directly under course root
128
            if ($this->tryDeleteFirstFolderByTitlePrefix($courseRoot, $prefix)) {
129
                $em->flush();
130
131
                return;
132
            }
133
134
            // Or under "Learning paths"
135
            $lpTop = $this->findChildNodeByTitle($courseRoot, 'Learning paths');
136
            if ($lpTop && $this->tryDeleteFirstFolderByTitlePrefix($lpTop, $prefix)) {
137
                $em->flush();
138
139
                return;
140
            }
141
        }
142
    }
143
144
    /**
145
     * Try to delete the first child folder whose title starts with $prefix under $parent.
146
     * Returns true if something was removed.
147
     */
148
    private function tryDeleteFirstFolderByTitlePrefix(ResourceNode $parent, string $prefix): bool
149
    {
150
        $em = $this->em();
151
        $qb = $em->createQueryBuilder()
152
            ->select('rn')
153
            ->from(ResourceNode::class, 'rn')
154
            ->where('rn.parent = :parent')
155
            ->andWhere('rn.title LIKE :prefix')
156
            ->setParameters(['parent' => $parent, 'prefix' => $prefix.'%'])
157
            ->setMaxResults(1)
158
        ;
159
160
        /** @var ResourceNode|null $node */
161
        $node = $qb->getQuery()->getOneOrNullResult();
162
        if ($node) {
163
            $em->remove($node);
164
165
            return true;
166
        }
167
168
        return false;
169
    }
170
171
    /**
172
     * Find the course Documents root node.
173
     *
174
     * Primary: parent = course.resourceNode
175
     * Fallback (legacy): parent IS NULL
176
     */
177
    public function getCourseDocumentsRootNode(Course $course): ?ResourceNode
178
    {
179
        $em = $this->em();
180
        $rt = $this->getResourceType();
181
        $courseNode = $course->getResourceNode();
182
183
        if ($courseNode) {
184
            $node = $em->createQuery(
185
                'SELECT rn
186
                   FROM Chamilo\CoreBundle\Entity\ResourceNode rn
187
                   JOIN rn.resourceType rt
188
                   JOIN rn.resourceLinks rl
189
                  WHERE rn.parent = :parent
190
                    AND rt = :rtype
191
                    AND rl.course = :course
192
               ORDER BY rn.id ASC'
193
            )
194
                ->setParameters([
195
                    'parent' => $courseNode,
196
                    'rtype' => $rt,
197
                    'course' => $course,
198
                ])
199
                ->setMaxResults(1)
200
                ->getOneOrNullResult()
201
            ;
202
203
            if ($node) {
204
                return $node;
205
            }
206
        }
207
208
        // Fallback for historical data (Documents root directly under NULL)
209
        return $em->createQuery(
210
            'SELECT rn
211
               FROM Chamilo\CoreBundle\Entity\ResourceNode rn
212
               JOIN rn.resourceType rt
213
               JOIN rn.resourceLinks rl
214
              WHERE rn.parent IS NULL
215
                AND rt = :rtype
216
                AND rl.course = :course
217
           ORDER BY rn.id ASC'
218
        )
219
            ->setParameters(['rtype' => $rt, 'course' => $course])
220
            ->setMaxResults(1)
221
            ->getOneOrNullResult()
222
        ;
223
    }
224
225
    /**
226
     * Ensure the course "Documents" root node exists.
227
     * Now parent = course.resourceNode (so the tool lists it under the course).
228
     */
229
    public function ensureCourseDocumentsRootNode(Course $course): ResourceNode
230
    {
231
        if ($root = $this->getCourseDocumentsRootNode($course)) {
232
            return $root;
233
        }
234
235
        $em = $this->em();
236
        $type = $this->getResourceType();
237
238
        /** @var User|null $user */
239
        $user = api_get_user_entity();
240
241
        $node = new ResourceNode();
242
        $node->setTitle('Documents');
243
        $node->setResourceType($type);
244
        $node->setPublic(false);
245
246
        if ($course->getResourceNode()) {
247
            $node->setParent($course->getResourceNode());
248
        }
249
250
        if ($user) {
251
            $node->setCreator($user);
252
        }
253
254
        $link = new ResourceLink();
255
        $link->setCourse($course);
256
        $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
257
        $node->addResourceLink($link);
258
259
        $em->persist($node);
260
        $em->flush();
261
262
        return $node;
263
    }
264
265
    /**
266
     * Create (if missing) a folder under $parent using the CDocument API path.
267
     * The folder is attached under the given $parent (not the course root),
268
     * and linked to the course (cid) and optionally the session (sid).
269
     */
270
    public function ensureFolder(
271
        Course $course,
272
        ResourceNode $parent,
273
        string $folderTitle,
274
        int $visibility = ResourceLink::VISIBILITY_DRAFT,
275
        ?Session $session = null
276
    ): ResourceNode {
277
        // Return if a child with this title already exists under the parent
278
        if ($child = $this->findChildNodeByTitle($parent, $folderTitle)) {
279
            return $child;
280
        }
281
282
        /** @var User|null $user */
283
        $user = api_get_user_entity();
284
285
        $doc = new CDocument();
286
        $doc->setTitle($folderTitle);
287
        $doc->setFiletype('folder');
288
289
        // IMPORTANT: attach to the given parent, not to the course root
290
        $doc->setParentResourceNode($parent->getId());
291
292
        // Link to course (and optional session)
293
        $link = [
294
            'cid' => $course->getId(),
295
            'visibility' => $visibility,
296
        ];
297
        if ($session && method_exists($session, 'getId')) {
298
            $link['sid'] = $session->getId();
299
        }
300
        $doc->setResourceLinkArray([$link]);
301
302
        if ($user) {
303
            $doc->setCreator($user);
304
        }
305
306
        $em = $this->em();
307
        $em->persist($doc);
308
        $em->flush();
309
310
        return $doc->getResourceNode();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $doc->getResourceNode() could return the type null which is incompatible with the type-hinted return Chamilo\CoreBundle\Entity\ResourceNode. Consider adding an additional type-check to rule them out.
Loading history...
311
    }
312
313
    /**
314
     * Create a file under $parent using the CDocument API path.
315
     * The file is linked to the course (cid) and optionally the session (sid).
316
     */
317
    public function createFileInFolder(
318
        Course $course,
319
        ResourceNode $parent,
320
        UploadedFile $uploaded,
321
        string $comment,
322
        int $visibility,
323
        ?Session $session = null
324
    ): ResourceNode {
325
        /** @var User|null $user */
326
        $user = api_get_user_entity();
327
328
        $title = $uploaded->getClientOriginalName();
329
330
        $doc = new CDocument();
331
        $doc->setTitle($title);
332
        $doc->setFiletype('file');
333
        $doc->setComment($comment);
334
        $doc->setParentResourceNode($parent->getId());
335
336
        $link = [
337
            'cid' => $course->getId(),
338
            'visibility' => $visibility,
339
        ];
340
        if ($session && method_exists($session, 'getId')) {
341
            $link['sid'] = $session->getId();
342
        }
343
        $doc->setResourceLinkArray([$link]);
344
345
        $doc->setUploadFile($uploaded);
346
347
        if ($user) {
348
            $doc->setCreator($user);
349
        }
350
351
        $em = $this->em();
352
        $em->persist($doc);
353
        $em->flush();
354
355
        return $doc->getResourceNode();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $doc->getResourceNode() could return the type null which is incompatible with the type-hinted return Chamilo\CoreBundle\Entity\ResourceNode. Consider adding an additional type-check to rule them out.
Loading history...
356
    }
357
358
    public function findChildNodeByTitle(ResourceNode $parent, string $title): ?ResourceNode
359
    {
360
        return $this->em()
361
            ->getRepository(ResourceNode::class)
362
            ->findOneBy([
363
                'parent' => $parent->getId(),
364
                'title' => $title,
365
            ])
366
        ;
367
    }
368
369
    /**
370
     * Fetches all document data for the given user/group using Doctrine ORM.
371
     *
372
     * @return CDocument[]
373
     */
374
    public function getAllDocumentDataByUserAndGroup(
375
        Course $course,
376
        string $path = '/',
377
        int $toGroupId = 0,
378
        ?int $toUserId = null,
379
        bool $search = false,
380
        ?Session $session = null
381
    ): array {
382
        $qb = $this->createQueryBuilder('d');
383
384
        $qb->innerJoin('d.resourceNode', 'rn')
385
            ->innerJoin('rn.resourceLinks', 'rl')
386
            ->where('rl.course = :course')
387
            ->setParameter('course', $course)
388
        ;
389
390
        // Session filtering
391
        if ($session) {
392
            $qb->andWhere('(rl.session = :session OR rl.session IS NULL)')
393
                ->setParameter('session', $session)
394
            ;
395
        } else {
396
            $qb->andWhere('rl.session IS NULL');
397
        }
398
399
        // Path filtering - convert document.lib.php logic to Doctrine
400
        if ('/' !== $path) {
401
            // The original uses LIKE with path patterns
402
            $pathPattern = rtrim($path, '/').'/%';
403
            $qb->andWhere('rn.title LIKE :pathPattern OR rn.title = :exactPath')
404
                ->setParameter('pathPattern', $pathPattern)
405
                ->setParameter('exactPath', ltrim($path, '/'))
406
            ;
407
408
            // Exclude deeper nested paths if not searching
409
            if (!$search) {
410
                // Exclude paths with additional slashes beyond the current level
411
                $excludePattern = rtrim($path, '/').'/%/%';
412
                $qb->andWhere('rn.title NOT LIKE :excludePattern')
413
                    ->setParameter('excludePattern', $excludePattern)
414
                ;
415
            }
416
        }
417
418
        // User/Group filtering
419
        if (null !== $toUserId) {
420
            if ($toUserId > 0) {
421
                $qb->andWhere('rl.user = :userId')
422
                    ->setParameter('userId', $toUserId)
423
                ;
424
            } else {
425
                $qb->andWhere('rl.user IS NULL');
426
            }
427
        } else {
428
            if ($toGroupId > 0) {
429
                $qb->andWhere('rl.group = :groupId')
430
                    ->setParameter('groupId', $toGroupId)
431
                ;
432
            } else {
433
                $qb->andWhere('rl.group IS NULL');
434
            }
435
        }
436
437
        // Exclude deleted documents (like %_DELETED_% in original)
438
        $qb->andWhere('rn.title NOT LIKE :deletedPattern')
439
            ->setParameter('deletedPattern', '%_DELETED_%')
440
        ;
441
442
        // Order by creation date (equivalent to last.iid DESC)
443
        $qb->orderBy('rn.createdAt', 'DESC')
444
            ->addOrderBy('rn.id', 'DESC')
445
        ;
446
447
        return $qb->getQuery()->getResult();
448
    }
449
450
    /**
451
     * Ensure "Learning paths" exists directly under the course resource node.
452
     * Links are created for course (and optional session) context.
453
     */
454
    public function ensureLearningPathSystemFolder(Course $course, ?Session $session = null): ResourceNode
455
    {
456
        $courseRoot = $course->getResourceNode();
457
        if (!$courseRoot instanceof ResourceNode) {
458
            throw new RuntimeException('Course has no ResourceNode root.');
459
        }
460
461
        // Try common i18n variants first
462
        $candidates = array_values(array_unique(array_filter([
463
            \function_exists('get_lang') ? get_lang('Learning paths') : null,
464
            \function_exists('get_lang') ? get_lang('Learning path') : null,
465
            'Learning paths',
466
            'Learning path',
467
        ])));
468
469
        foreach ($candidates as $title) {
470
            if ($child = $this->findChildNodeByTitle($courseRoot, $title)) {
471
                return $child;
472
            }
473
        }
474
475
        // Create "Learning paths" directly under the course root
476
        return $this->ensureFolder(
477
            $course,
478
            $courseRoot,
479
            'Learning paths',
480
            ResourceLink::VISIBILITY_DRAFT,
481
            $session
482
        );
483
    }
484
485
    private function safeTitle(string $name): string
486
    {
487
        $name = trim($name);
488
        $name = str_replace(['/', '\\'], '-', $name);
489
490
        return preg_replace('/\s+/', ' ', $name) ?: 'Untitled';
491
    }
492
493
    private function em(): EntityManagerInterface
494
    {
495
        /** @var EntityManagerInterface $em */
496
        return $this->getEntityManager();
497
    }
498
}
499