Passed
Pull Request — master (#5625)
by Angel Fernando Quiroz
08:06 queued 01:11
created

ResourceNode::setResourceFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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