Passed
Pull Request — master (#5143)
by Angel Fernando Quiroz
08:34
created

ResourceNode::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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