Passed
Pull Request — master (#5625)
by Angel Fernando Quiroz
07:16
created

ResourceNode::addResourceFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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