Passed
Pull Request — master (#7158)
by
unknown
11:01
created

ResourceNode::getChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Entity;
8
9
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
10
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
11
use ApiPlatform\Metadata\ApiFilter;
12
use ApiPlatform\Metadata\ApiResource;
13
use ApiPlatform\Metadata\Delete;
14
use ApiPlatform\Metadata\Get;
15
use ApiPlatform\Metadata\GetCollection;
16
use ApiPlatform\Metadata\Patch;
17
use ApiPlatform\Metadata\Put;
18
use ApiPlatform\Serializer\Filter\PropertyFilter;
19
use Chamilo\CoreBundle\Entity\Listener\ResourceNodeListener;
20
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
21
use Chamilo\CoreBundle\Traits\TimestampableAgoTrait;
22
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
23
use Chamilo\CourseBundle\Entity\CGroup;
24
use Chamilo\CourseBundle\Entity\CShortcut;
25
use DateTime;
26
use Doctrine\Common\Collections\ArrayCollection;
27
use Doctrine\Common\Collections\Collection;
28
use Doctrine\Common\Collections\Criteria;
29
use Doctrine\ORM\Mapping as ORM;
30
use Gedmo\Mapping\Annotation as Gedmo;
31
use InvalidArgumentException;
32
use Stringable;
33
use Symfony\Component\Routing\RouterInterface;
34
use Symfony\Component\Serializer\Annotation\Groups;
35
use Symfony\Component\Serializer\Annotation\MaxDepth;
36
use Symfony\Component\Uid\Uuid;
37
use Symfony\Component\Uid\UuidV4;
38
use Symfony\Component\Validator\Constraints as Assert;
39
40
// *     attributes={"security"="is_granted('ROLE_ADMIN')"},
41
42
/**
43
 * Base entity for all resources.
44
 */
45
#[ORM\Table(name: 'resource_node')]
46
#[ORM\Entity(repositoryClass: ResourceNodeRepository::class)]
47
#[ORM\HasLifecycleCallbacks]
48
#[ORM\EntityListeners([ResourceNodeListener::class])]
49
#[Gedmo\Tree(type: 'materializedPath')]
50
#[ApiResource(
51
    operations: [
52
        new Get(),
53
        new Put(),
54
        new Patch(),
55
        new Delete(),
56
        new GetCollection(),
57
    ],
58
    normalizationContext: [
59
        'groups' => [
60
            'resource_node:read',
61
            'document:read',
62
            'personal_file:read',
63
        ],
64
    ],
65
    denormalizationContext: [
66
        'groups' => [
67
            'resource_node:write',
68
            'document:write',
69
            'personal_file:write',
70
        ],
71
    ]
72
)]
73
#[ApiFilter(filterClass: OrderFilter::class, properties: ['id', 'title', 'createdAt', 'updatedAt', 'firstResourceFile.size'])]
74
#[ApiFilter(filterClass: PropertyFilter::class)]
75
#[ApiFilter(filterClass: SearchFilter::class, properties: ['title' => 'partial'])]
76
class ResourceNode implements Stringable
77
{
78
    use TimestampableAgoTrait;
79
    use TimestampableTypedEntity;
80
81
    public const PATH_SEPARATOR = '/';
82
83
    #[Groups(['resource_node:read', 'document:read', 'ctool:read', 'user_json:read', 'course:read'])]
84
    #[ORM\Id]
85
    #[ORM\Column(type: 'integer')]
86
    #[ORM\GeneratedValue(strategy: 'AUTO')]
87
    protected ?int $id = null;
88
89
    #[Groups(['resource_node:read', 'resource_node:write', 'document:read', 'document:write'])]
90
    #[Assert\NotBlank]
91
    #[Gedmo\TreePathSource]
92
    #[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)]
93
    protected string $title;
94
95
    #[Assert\NotBlank]
96
    #[Gedmo\Slug(fields: ['title'])]
97
    #[ORM\Column(name: 'slug', type: 'string', length: 255, nullable: false)]
98
    protected string $slug;
99
100
    #[Groups(['resource_node:read'])]
101
    #[Assert\NotNull]
102
    #[ORM\ManyToOne(targetEntity: ResourceType::class, inversedBy: 'resourceNodes', cascade: ['persist'])]
103
    #[ORM\JoinColumn(name: 'resource_type_id', referencedColumnName: 'id', nullable: false)]
104
    protected ResourceType $resourceType;
105
106
    #[ORM\ManyToOne(targetEntity: ResourceFormat::class, inversedBy: 'resourceNodes')]
107
    #[ORM\JoinColumn(name: 'resource_format_id', referencedColumnName: 'id')]
108
    protected ?ResourceFormat $resourceFormat = null;
109
110
    /**
111
     * Optional language for the node.
112
     * This is used for indexing and future-proof resource variations.
113
     */
114
    #[Groups(['resource_node:read', 'document:read', 'personal_file:read'])]
115
    #[ORM\ManyToOne(targetEntity: Language::class)]
116
    #[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
117
    protected ?Language $language = null;
118
119
    /**
120
     * @var Collection<int, ResourceLink>
121
     */
122
    #[Groups(['ctool:read', 'c_tool_intro:read'])]
123
    #[ORM\OneToMany(mappedBy: 'resourceNode', targetEntity: ResourceLink::class, cascade: ['persist', 'remove'])]
124
    protected Collection $resourceLinks;
125
126
    #[Assert\NotNull]
127
    #[Groups(['resource_node:read', 'resource_node:write', 'document:write'])]
128
    #[ORM\ManyToOne(targetEntity: User::class, cascade: ['persist'], inversedBy: 'resourceNodes')]
129
    #[ORM\JoinColumn(name: 'creator_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
130
    protected ?User $creator = null;
131
132
    #[Groups(['resource_node:read', 'student_publication:read'])]
133
    #[MaxDepth(1)]
134
    #[ORM\JoinColumn(name: 'parent_id', onDelete: 'CASCADE')]
135
    #[ORM\ManyToOne(targetEntity: self::class, cascade: ['persist'], inversedBy: 'children')]
136
    #[Gedmo\TreeParent]
137
    protected ?ResourceNode $parent = null;
138
139
    /**
140
     * @var Collection<int, ResourceNode>
141
     */
142
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])]
143
    #[ORM\OrderBy(['id' => 'ASC'])]
144
    protected Collection $children;
145
146
    #[Gedmo\TreeLevel]
147
    #[ORM\Column(name: 'level', type: 'integer', nullable: true)]
148
    protected ?int $level = null;
149
150
    #[Groups(['resource_node:read', 'document:read'])]
151
    #[Gedmo\TreePath(separator: '/', appendId: true)]
152
    #[ORM\Column(name: 'path', type: 'text', nullable: true)]
153
    protected ?string $path = null;
154
155
    /**
156
     * Shortcut to access Course resource from ResourceNode.
157
     * Groups({"resource_node:read", "course:read"}).
158
     *
159
     * ORM\OneToOne(targetEntity="Chamilo\CoreBundle\Entity\Illustration", mappedBy="resourceNode")
160
     */
161
    // protected $illustration;
162
163
    /**
164
     * @var Collection<int, ResourceComment>
165
     */
166
    #[ORM\OneToMany(mappedBy: 'resourceNode', targetEntity: ResourceComment::class, cascade: ['persist', 'remove'])]
167
    protected Collection $comments;
168
169
    #[Groups(['resource_node:read', 'document:read'])]
170
    #[Gedmo\Timestampable(on: 'create')]
171
    #[ORM\Column(type: 'datetime')]
172
    protected DateTime $createdAt;
173
174
    #[Groups(['resource_node:read', 'document:read'])]
175
    #[Gedmo\Timestampable(on: 'update')]
176
    #[ORM\Column(type: 'datetime')]
177
    protected DateTime $updatedAt;
178
179
    #[Groups(['resource_node:read', 'document:read'])]
180
    protected bool $fileEditableText;
181
182
    #[Groups(['resource_node:read', 'document:read'])]
183
    #[ORM\Column(type: 'boolean')]
184
    protected bool $public;
185
186
    protected ?string $content = null;
187
188
    #[ORM\OneToOne(mappedBy: 'shortCutNode', targetEntity: CShortcut::class, cascade: ['persist', 'remove'])]
189
    protected ?CShortcut $shortCut = null;
190
191
    #[Groups(['resource_node:read', 'document:read'])]
192
    #[ORM\Column(type: 'uuid', unique: true)]
193
    protected ?UuidV4 $uuid = null;
194
195
    /**
196
     * ResourceFile available file for this node.
197
     *
198
     * @var Collection<int, ResourceFile>
199
     */
200
    #[Groups(['resource_node:read', 'resource_node:write', 'document:read', 'document:write', 'message:read', 'personal_file:read'])]
201
    #[ORM\OneToMany(
202
        mappedBy: 'resourceNode',
203
        targetEntity: ResourceFile::class,
204
        cascade: ['persist', 'remove'],
205
        fetch: 'EXTRA_LAZY',
206
    )]
207
    private Collection $resourceFiles;
208
209
    public function __construct()
210
    {
211
        $this->public = false;
212
        $this->uuid = Uuid::v4();
213
        $this->children = new ArrayCollection();
214
        $this->resourceLinks = new ArrayCollection();
215
        $this->comments = new ArrayCollection();
216
        $this->createdAt = new DateTime();
217
        $this->fileEditableText = false;
218
        $this->resourceFiles = new ArrayCollection();
219
    }
220
221
    public function __toString(): string
222
    {
223
        return $this->getPathForDisplay();
224
    }
225
226
    /**
227
     * Returns the path cleaned from its ids.
228
     * Eg.: "Root/subdir/file.txt".
229
     */
230
    public function getPathForDisplay(): string
231
    {
232
        return $this->path;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->path could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
233
        // return $this->convertPathForDisplay($this->path);
234
    }
235
236
    public function getUuid(): ?UuidV4
237
    {
238
        return $this->uuid;
239
    }
240
241
    public function hasCreator(): bool
242
    {
243
        return isset($this->creator) && null !== $this->creator;
244
    }
245
246
    public function getCreator(): ?User
247
    {
248
        return $this->creator;
249
    }
250
251
    public function setCreator(?User $creator): self
252
    {
253
        $this->creator = $creator;
254
255
        return $this;
256
    }
257
258
    /**
259
     * Get the node language (can be null if unknown/multilingual).
260
     */
261
    public function getLanguage(): ?Language
262
    {
263
        return $this->language;
264
    }
265
266
    /**
267
     * Set the node language (nullable by design).
268
     */
269
    public function setLanguage(?Language $language): self
270
    {
271
        $this->language = $language;
272
273
        return $this;
274
    }
275
276
    /**
277
     * Returns the children resource instances.
278
     *
279
     * @return Collection<int, ResourceNode>
280
     */
281
    public function getChildren(): Collection
282
    {
283
        return $this->children;
284
    }
285
286
    public function addChild(self $resourceNode): static
287
    {
288
        if (!$this->children->contains($resourceNode)) {
289
            $this->children->add($resourceNode);
290
291
            $resourceNode->setParent($this);
292
        }
293
294
        return $this;
295
    }
296
297
    /**
298
     * Returns the parent resource.
299
     */
300
    public function getParent(): ?self
301
    {
302
        return $this->parent;
303
    }
304
305
    /**
306
     * Sets the parent resource.
307
     */
308
    public function setParent(?self $parent = null): self
309
    {
310
        $this->parent = $parent;
311
312
        return $this;
313
    }
314
315
    /**
316
     * Return the lvl value of the resource in the tree.
317
     */
318
    public function getLevel(): int
319
    {
320
        return $this->level;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->level could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
321
    }
322
323
    /**
324
     * Returns the "raw" path of the resource
325
     * (the path merge names and ids of all items).
326
     * Eg.: "Root-1/subdir-2/file.txt-3/".
327
     */
328
    public function getPath(): ?string
329
    {
330
        return $this->path;
331
    }
332
333
    /**
334
     * @return Collection|ResourceComment[]
335
     */
336
    public function getComments(): array|Collection
337
    {
338
        return $this->comments;
339
    }
340
341
    public function addComment(ResourceComment $comment): self
342
    {
343
        $comment->setResourceNode($this);
344
        $this->comments->add($comment);
345
346
        return $this;
347
    }
348
349
    public function getPathForDisplayToArray(?int $baseRoot = null): array
350
    {
351
        $parts = explode(self::PATH_SEPARATOR, $this->path);
352
        $list = [];
353
        foreach ($parts as $part) {
354
            $parts = explode('-', $part);
355
            if (empty($parts[1])) {
356
                continue;
357
            }
358
            $value = $parts[0];
359
            $id = $parts[1];
360
            if (!empty($baseRoot) && $id < $baseRoot) {
361
                continue;
362
            }
363
            $list[$id] = $value;
364
        }
365
366
        return $list;
367
    }
368
369
    public function getPathForDisplayRemoveBase(string $base): string
370
    {
371
        $path = str_replace($base, '', $this->path);
372
373
        return $this->convertPathForDisplay($path);
374
    }
375
376
    /**
377
     * Convert a path for display: remove ids.
378
     */
379
    public function convertPathForDisplay(string $path): string
380
    {
381
        $pathForDisplay = preg_replace('/-\d+\\'.self::PATH_SEPARATOR.'/', '/', $path);
382
        if (null !== $pathForDisplay && '' !== $pathForDisplay) {
383
            $pathForDisplay = substr_replace($pathForDisplay, '', -1);
384
        }
385
386
        return $pathForDisplay;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $pathForDisplay could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
387
    }
388
389
    public function getSlug(): string
390
    {
391
        return $this->slug;
392
    }
393
394
    public function setSlug(string $slug): self
395
    {
396
        if (str_contains(self::PATH_SEPARATOR, $slug)) {
397
            $message = 'Invalid character "'.self::PATH_SEPARATOR.'" in resource name';
398
399
            throw new InvalidArgumentException($message);
400
        }
401
        $this->slug = $slug;
402
403
        return $this;
404
    }
405
406
    public function getTitle(): string
407
    {
408
        return $this->title;
409
    }
410
411
    public function setTitle(string $title): self
412
    {
413
        $title = str_replace('/', '-', $title);
414
        $this->title = $title;
415
416
        return $this;
417
    }
418
419
    public function getResourceFormat(): ?ResourceFormat
420
    {
421
        return $this->resourceFormat;
422
    }
423
424
    public function setResourceFormat(?ResourceFormat $resourceFormat): self
425
    {
426
        $this->resourceFormat = $resourceFormat;
427
428
        return $this;
429
    }
430
431
    /**
432
     * @return Collection<int, ResourceLink>
433
     */
434
    public function getResourceLinks(): Collection
435
    {
436
        return $this->resourceLinks;
437
    }
438
439
    public function getResourceLinkByContext(
440
        ?Course $course = null,
441
        ?Session $session = null,
442
        ?CGroup $group = null,
443
        ?Usergroup $usergroup = null,
444
        ?User $user = null,
445
    ): ?ResourceLink {
446
        $criteria = Criteria::create();
447
        $criteria->where(
448
            Criteria::expr()->eq('resourceTypeGroup', $this->resourceType->getId())
449
        );
450
451
        if ($course) {
452
            $criteria->andWhere(
453
                Criteria::expr()->eq('course', $course)
454
            );
455
        }
456
457
        if ($session) {
458
            $criteria->andWhere(
459
                Criteria::expr()->eq('session', $session)
460
            );
461
        }
462
463
        if ($usergroup) {
464
            $criteria->andWhere(
465
                Criteria::expr()->eq('userGroup', $usergroup)
466
            );
467
        }
468
469
        if ($group) {
470
            $criteria->andWhere(
471
                Criteria::expr()->eq('group', $group)
472
            );
473
        }
474
475
        if ($user) {
476
            $criteria->andWhere(
477
                Criteria::expr()->eq('user', $user)
478
            );
479
        }
480
481
        $first = $this
482
            ->resourceLinks
483
            ->matching($criteria)
484
            ->first()
485
        ;
486
487
        return $first ?: null;
488
    }
489
490
    public function setResourceLinks(Collection $resourceLinks): self
491
    {
492
        $this->resourceLinks = $resourceLinks;
493
494
        return $this;
495
    }
496
497
    public function addResourceLink(ResourceLink $link): self
498
    {
499
        $link->setResourceNode($this);
500
501
        $this->resourceLinks->add($link);
502
503
        return $this;
504
    }
505
506
    public function hasEditableTextContent(): bool
507
    {
508
        if ($resourceFile = $this->resourceFiles->first()) {
509
            $mimeType = $resourceFile->getMimeType();
510
511
            if (str_contains($mimeType, 'text')) {
512
                return true;
513
            }
514
        }
515
516
        return false;
517
    }
518
519
    public function getIcon(?string $additionalClass = null): string
520
    {
521
        $class = 'fa fa-folder';
522
        if ($this->hasResourceFile()) {
523
            $class = 'far fa-file';
524
            if ($this->isResourceFileAnImage()) {
525
                $class = 'far fa-file-image';
526
            }
527
            if ($this->isResourceFileAVideo()) {
528
                $class = 'far fa-file-video';
529
            }
530
        }
531
532
        if ($additionalClass) {
533
            $class .= " $additionalClass";
534
        }
535
536
        return '<i class="'.$class.'"></i>';
537
    }
538
539
    public function isResourceFileAnImage(): bool
540
    {
541
        if ($resourceFile = $this->resourceFiles->first()) {
542
            $mimeType = $resourceFile->getMimeType();
543
            if (str_contains($mimeType, 'image')) {
544
                return true;
545
            }
546
        }
547
548
        return false;
549
    }
550
551
    public function isResourceFileAVideo(): bool
552
    {
553
        if ($resourceFile = $this->resourceFiles->first()) {
554
            $mimeType = $resourceFile->getMimeType();
555
            if (str_contains($mimeType, 'video')) {
556
                return true;
557
            }
558
        }
559
560
        return false;
561
    }
562
563
    public function getThumbnail(RouterInterface $router): string
564
    {
565
        if ($this->isResourceFileAnImage()) {
566
            $params = [
567
                'id' => $this->getId(),
568
                'tool' => $this->getResourceType()->getTool(),
569
                'type' => $this->getResourceType()->getTitle(),
570
                'filter' => 'editor_thumbnail',
571
            ];
572
            $url = $router->generate('chamilo_core_resource_view', $params);
573
574
            return \sprintf("<img src='%s'/>", $url);
575
        }
576
577
        return $this->getIcon('fa-3x');
578
    }
579
580
    /**
581
     * Returns the resource id.
582
     */
583
    public function getId(): ?int
584
    {
585
        return $this->id;
586
    }
587
588
    public function getResourceType(): ResourceType
589
    {
590
        return $this->resourceType;
591
    }
592
593
    public function setResourceType(ResourceType $resourceType): self
594
    {
595
        $this->resourceType = $resourceType;
596
597
        return $this;
598
    }
599
600
    public function getContent(): ?string
601
    {
602
        return $this->content;
603
    }
604
605
    public function setContent(string $content): self
606
    {
607
        $this->content = $content;
608
609
        return $this;
610
    }
611
612
    public function getShortCut(): ?CShortcut
613
    {
614
        return $this->shortCut;
615
    }
616
617
    public function setShortCut(?CShortcut $shortCut): self
618
    {
619
        $this->shortCut = $shortCut;
620
621
        return $this;
622
    }
623
624
    public function isPublic(): bool
625
    {
626
        return $this->public;
627
    }
628
629
    public function setPublic(bool $public): self
630
    {
631
        $this->public = $public;
632
633
        return $this;
634
    }
635
636
    public function hasResourceFile(): bool
637
    {
638
        return $this->resourceFiles->count() > 0;
639
    }
640
641
    /**
642
     * @return Collection<int, ResourceFile>
643
     */
644
    public function getResourceFiles(): Collection
645
    {
646
        return $this->resourceFiles;
647
    }
648
649
    public function addResourceFile(ResourceFile $resourceFile): static
650
    {
651
        if (!$this->resourceFiles->contains($resourceFile)) {
652
            $this->resourceFiles->add($resourceFile);
653
            $resourceFile->setResourceNode($this);
654
        }
655
656
        return $this;
657
    }
658
659
    public function removeResourceFile(ResourceFile $resourceFile): static
660
    {
661
        if ($this->resourceFiles->removeElement($resourceFile)) {
662
            // set the owning side to null (unless already changed)
663
            if ($resourceFile->getResourceNode() === $this) {
664
                $resourceFile->setResourceNode(null);
665
            }
666
        }
667
668
        return $this;
669
    }
670
671
    #[Groups(['resource_node:read', 'document:read', 'message:read', 'personal_file:read'])]
672
    public function getFirstResourceFile(): ?ResourceFile
673
    {
674
        return $this->resourceFiles->first() ?: null;
675
    }
676
}
677