Passed
Push — master ( 5c4b0c...475b00 )
by
unknown
04:15
created

Card::setIsbn()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 8
ccs 3
cts 4
cp 0.75
rs 10
cc 3
nc 2
nop 1
crap 3.1406
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Model;
6
7
use Application\Api\FileException;
8
use Application\Api\Input\Operator\ArtistOrTechniqueAuthorOperatorType;
9
use Application\Api\Input\Operator\CardYearRangeOperatorType;
10
use Application\Api\Input\Operator\DatingYearRangeOperatorType;
11
use Application\Api\Input\Operator\LocalityOrInstitutionLocalityOperatorType;
12
use Application\Api\Input\Operator\LocationOperatorType;
13
use Application\Api\Input\Operator\NameOrExpandedNameOperatorType;
14
use Application\Api\Input\Sorting\Artists;
15
use Application\Api\Input\Sorting\InstitutionLocality;
16
use Application\DBAL\Types\SiteType;
17
use Application\Repository\CardRepository;
18
use Application\Service\DatingRule;
19
use Application\Traits\CardSimpleProperties;
20
use Application\Traits\HasAddress;
21
use Application\Traits\HasCode;
22
use Application\Traits\HasFileSize;
23
use Application\Traits\HasImage;
24
use Application\Traits\HasInstitution;
25
use Application\Traits\HasParentInterface;
26
use Application\Traits\HasRichTextName;
27
use Application\Traits\HasSite;
28
use Application\Traits\HasSiteInterface;
29
use Application\Traits\HasValidation;
30
use Application\Traits\HasYearRange;
31
use Doctrine\Common\Collections\ArrayCollection;
32
use Doctrine\Common\Collections\Collection as DoctrineCollection;
33
use Doctrine\ORM\Mapping as ORM;
34
use Ecodev\Felix\Api\Exception;
35
use Ecodev\Felix\Model\Image;
36
use Ecodev\Felix\Utility;
37
use GraphQL\Doctrine\Attribute as API;
38
use GraphQL\Doctrine\Definition\EntityID;
39
use Imagine\Filter\Basic\Autorotate;
40
use Imagine\Image\ImagineInterface;
41
use InvalidArgumentException;
42
use Psr\Http\Message\UploadedFileInterface;
43
use Throwable;
44
45
/**
46
 * A card containing an image and some information about it.
47
 */
48
#[ORM\Index(name: 'card_name_idx', columns: ['name'])]
49
#[ORM\Index(name: 'card_plain_name_idx', columns: ['plain_name'])]
50
#[ORM\Index(name: 'card_locality_idx', columns: ['locality'])]
51
#[ORM\Index(name: 'card_area_idx', columns: ['area'])]
52
#[ORM\UniqueConstraint(name: 'unique_code', columns: ['code', 'site'])]
53
#[API\Filter(field: 'nameOrExpandedName', operator: NameOrExpandedNameOperatorType::class, type: 'string')]
54
#[API\Filter(field: 'artistOrTechniqueAuthor', operator: ArtistOrTechniqueAuthorOperatorType::class, type: 'string')]
55
#[API\Filter(field: 'localityOrInstitutionLocality', operator: LocalityOrInstitutionLocalityOperatorType::class, type: 'string')]
56
#[API\Filter(field: 'datingYearRange', operator: DatingYearRangeOperatorType::class, type: 'int')]
57
#[API\Filter(field: 'cardYearRange', operator: CardYearRangeOperatorType::class, type: 'int')]
58
#[API\Filter(field: 'custom', operator: LocationOperatorType::class, type: 'string')]
59
#[API\Sorting(Artists::class)]
60
#[API\Sorting(InstitutionLocality::class)]
61
#[API\Sorting(\Application\Api\Input\Sorting\DocumentType::class)]
62
#[ORM\HasLifecycleCallbacks]
63
#[ORM\Entity(CardRepository::class)]
64
class Card extends AbstractModel implements HasSiteInterface, Image
65
{
66
    use CardSimpleProperties;
67
    use HasAddress;
68
    use HasCode;
69
    use HasFileSize;
70
    use HasImage {
71
        setFile as traitSetFile;
72
    }
73
    use HasInstitution;
74
    use HasRichTextName;
75
    use HasSite;
76
    use HasValidation;
77
    use HasYearRange;
78
79
    private const IMAGE_PATH = 'data/images/';
80
81
    final public const VISIBILITY_PRIVATE = 'private';
82
    final public const VISIBILITY_MEMBER = 'member';
83
    final public const VISIBILITY_PUBLIC = 'public';
84
85
    #[ORM\Column(type: 'CardVisibility', options: ['default' => self::VISIBILITY_PRIVATE])]
86
    private string $visibility = self::VISIBILITY_PRIVATE;
87
88
    #[ORM\Column(type: 'integer')]
89
    private int $width = 0;
90
91
    #[ORM\Column(type: 'integer')]
92
    private int $height = 0;
93
94
    #[ORM\Column(type: 'string', options: ['default' => ''])]
95
    private string $dating = '';
96
97
    /**
98
     * This is a form of cache of all artist names whose only purpose is to be able
99
     * to search on artists more easily. It is automatically maintained via DB triggers.
100
     */
101
    #[API\Exclude]
102
    #[ORM\Column(type: 'text', options: ['default' => ''])]
103
    private string $cachedArtistNames = '';
104
105
    /**
106
     * @var DoctrineCollection<Collection>
107
     */
108
    #[ORM\ManyToMany(targetEntity: Collection::class)]
109
    private DoctrineCollection $collections;
110
111
    /**
112
     * @var DoctrineCollection<Artist>
113
     */
114
    #[ORM\ManyToMany(targetEntity: Artist::class)]
115
    private DoctrineCollection $artists;
116
117
    /**
118
     * @var DoctrineCollection<AntiqueName>
119
     */
120
    #[ORM\ManyToMany(targetEntity: AntiqueName::class)]
121
    private DoctrineCollection $antiqueNames;
122
123
    /**
124
     * @var DoctrineCollection<Tag>
125
     */
126
    #[ORM\ManyToMany(targetEntity: Tag::class)]
127
    private DoctrineCollection $tags;
128
129
    /**
130
     * @var DoctrineCollection<Dating>
131
     */
132
    #[ORM\OneToMany(targetEntity: Dating::class, mappedBy: 'card')]
133
    private DoctrineCollection $datings;
134
135
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
136
    #[ORM\ManyToOne(targetEntity: self::class)]
137
    private ?Card $original = null;
138
139
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
140
    #[ORM\ManyToOne(targetEntity: DocumentType::class)]
141
    private ?DocumentType $documentType = null;
142
143
    /**
144
     * @var DoctrineCollection<Domain>
145
     */
146
    #[ORM\ManyToMany(targetEntity: Domain::class)]
147
    private DoctrineCollection $domains;
148
149
    /**
150
     * @var DoctrineCollection<Period>
151
     */
152
    #[ORM\ManyToMany(targetEntity: Period::class)]
153
    private DoctrineCollection $periods;
154
155
    /**
156
     * @var DoctrineCollection<Material>
157
     */
158
    #[ORM\ManyToMany(targetEntity: Material::class)]
159
    private DoctrineCollection $materials;
160
161
    /**
162
     * @var DoctrineCollection<Card>
163
     */
164
    #[ORM\ManyToMany(targetEntity: self::class)]
165
    private DoctrineCollection $cards;
166
167
    /**
168
     * There is actually 0 to 1 change, never more. And this is
169
     * enforced by DB unique constraints on the mapping side.
170
     *
171
     * @var DoctrineCollection<Change>
172
     */
173
    #[ORM\OneToMany(targetEntity: Change::class, mappedBy: 'suggestion')]
174
    private DoctrineCollection $changes;
175
176
    #[ORM\Column(type: 'string', length: 191)]
177
    private string $documentSize = '';
178
179
    #[ORM\Column(name: 'legacy_id', type: 'integer', nullable: true)]
180
    private ?int $legacyId = null;
181
182
    /**
183
     * Constructor.
184
     */
185 49
    public function __construct(string $name = '')
186
    {
187 49
        $this->setName($name);
188
189 49
        $this->changes = new ArrayCollection();
190 49
        $this->collections = new ArrayCollection();
191 49
        $this->artists = new ArrayCollection();
192 49
        $this->antiqueNames = new ArrayCollection();
193 49
        $this->tags = new ArrayCollection();
194 49
        $this->datings = new ArrayCollection();
195 49
        $this->cards = new ArrayCollection();
196 49
        $this->domains = new ArrayCollection();
197 49
        $this->periods = new ArrayCollection();
198 49
        $this->materials = new ArrayCollection();
199
    }
200
201
    /**
202
     * Return whether this is publicly available to everybody, or only member, or only owner.
203
     */
204 17
    #[API\Field(type: 'Application\Api\Enum\CardVisibilityType')]
205
    public function getVisibility(): string
206
    {
207 17
        return $this->visibility;
208
    }
209
210
    /**
211
     * Set whether this is publicly available to everybody, or only member, or only owner.
212
     */
213 27
    #[API\Input(type: 'Application\Api\Enum\CardVisibilityType')]
214
    public function setVisibility(string $visibility): void
215
    {
216 27
        if ($this->visibility === $visibility) {
217 11
            return;
218
        }
219
220 24
        $user = User::getCurrent();
221 24
        if ($visibility === self::VISIBILITY_PUBLIC && $user->getRole() !== User::ROLE_ADMINISTRATOR) {
222 2
            throw new Exception('Only administrator can make a card public');
223
        }
224
225 23
        $this->visibility = $visibility;
226
    }
227
228
    /**
229
     * Get collections this card belongs to.
230
     */
231 10
    #[API\Field(type: 'Collection[]')]
232
    public function getCollections(): DoctrineCollection
233
    {
234 10
        return $this->collections;
235
    }
236
237
    /**
238
     * Get the card dating.
239
     *
240
     * This is a free form string that will be parsed to **try** and extract
241
     * some actual date range of dates. Any string is valid, but some parseable
242
     * values would typically be:
243
     *
244
     * - (1620-1652)
245
     * - 01.05.1917
246
     * - XIIIe siècle
247
     * - 1927
248
     * - c. 1100
249
     * - Fin du XIIe siècle
250
     */
251 5
    public function getDating(): string
252
    {
253 5
        return $this->dating;
254
    }
255
256
    /**
257
     * Set the card dating.
258
     *
259
     * This is a free form string that will be parsed to **try** and extract
260
     * some actual date range of dates. Any string is valid, but some parseable
261
     * values would typically be:
262
     *
263
     * - (1620-1652)
264
     * - 01.05.1917
265
     * - XIIIe siècle
266
     * - 1927
267
     * - c. 1100
268
     * - Fin du XIIe siècle
269
     */
270 10
    public function setDating(string $dating): void
271
    {
272 10
        if ($dating === $this->dating) {
273 1
            return;
274
        }
275 10
        $this->dating = $dating;
276
277 10
        $this->computeDatings();
278
    }
279
280
    /**
281
     * Return the automatically computed dating periods.
282
     */
283 4
    #[API\Field(type: 'Dating[]')]
284
    public function getDatings(): DoctrineCollection
285
    {
286 4
        return $this->datings;
287
    }
288
289
    /**
290
     * Set all artists at once by their names.
291
     *
292
     * Non-existing artists will be created automatically.
293
     *
294
     * @param null|string[] $artistNames
295
     */
296 9
    public function setArtists(?array $artistNames): void
297
    {
298 9
        if (null === $artistNames) {
0 ignored issues
show
introduced by
The condition null === $artistNames is always false.
Loading history...
299
            return;
300
        }
301
302 9
        $artistRepository = _em()->getRepository(Artist::class);
303 9
        $newArtists = $artistRepository->getOrCreateByNames($artistNames, $this->getSite());
304
305 9
        $oldIds = Utility::modelToId($this->artists->toArray());
306 9
        sort($oldIds);
307
308 9
        $newIds = Utility::modelToId($newArtists);
309 9
        sort($newIds);
310
311 9
        if ($oldIds === $newIds && !in_array(null, $oldIds, true) && !in_array(null, $newIds, true)) {
312
            return;
313
        }
314
315 9
        $this->artists->clear();
316 9
        foreach ($newArtists as $a) {
317 9
            $this->artists->add($a);
318
        }
319
    }
320
321
    /**
322
     * Set all materials at once.
323
     *
324
     * @param null|EntityID[] $materials
325
     */
326 7
    #[API\Input(type: '?MaterialID[]')]
327
    public function setMaterials(?array $materials): void
328
    {
329 7
        if (null === $materials) {
0 ignored issues
show
introduced by
The condition null === $materials is always false.
Loading history...
330
            return;
331
        }
332
333 7
        $this->setEntireCollection($materials, $this->materials, Material::class);
334 7
        $this->addEntireHierarchy($this->materials);
335
    }
336
337
    /**
338
     * Set all antiqueNames at once.
339
     *
340
     * @param null|EntityID[] $antiqueNames
341
     */
342 7
    #[API\Input(type: '?AntiqueNameID[]')]
343
    public function setAntiqueNames(?array $antiqueNames): void
344
    {
345 7
        if (null === $antiqueNames) {
0 ignored issues
show
introduced by
The condition null === $antiqueNames is always false.
Loading history...
346
            return;
347
        }
348
349 7
        $this->setEntireCollection($antiqueNames, $this->antiqueNames, AntiqueName::class);
350
    }
351
352
    /**
353
     * Set all domains at once.
354
     *
355
     * @param null|EntityID[] $domains
356
     */
357
    #[API\Input(type: '?DomainID[]')]
358
    public function setDomains(?array $domains): void
359
    {
360
        if (null === $domains) {
0 ignored issues
show
introduced by
The condition null === $domains is always false.
Loading history...
361
            return;
362
        }
363
364
        $this->setEntireCollection($domains, $this->domains, Domain::class);
365
    }
366
367
    /**
368
     * Set all periods at once.
369
     *
370
     * @param null|EntityID[] $periods
371
     */
372 7
    #[API\Input(type: '?PeriodID[]')]
373
    public function setPeriods(?array $periods): void
374
    {
375 7
        if (null === $periods) {
0 ignored issues
show
introduced by
The condition null === $periods is always false.
Loading history...
376
            return;
377
        }
378
379 7
        $this->setEntireCollection($periods, $this->periods, Period::class);
380
    }
381
382
    /**
383
     * Set all tags at once.
384
     *
385
     * @param null|EntityID[] $tags
386
     */
387 8
    #[API\Input(type: '?TagID[]')]
388
    public function setTags(?array $tags): void
389
    {
390 8
        if (null === $tags) {
0 ignored issues
show
introduced by
The condition null === $tags is always false.
Loading history...
391
            return;
392
        }
393
394 8
        $this->setEntireCollection($tags, $this->tags, Tag::class);
395 8
        $this->addEntireHierarchy($this->tags);
396
    }
397
398 8
    private function setEntireCollection(array $entityIds, DoctrineCollection $collection, string $class): void
399
    {
400 8
        $oldIds = Utility::modelToId($collection->toArray());
401 8
        sort($oldIds);
402
403 8
        $ids = Utility::modelToId($entityIds);
404 8
        sort($ids);
405
406 8
        if ($oldIds === $ids && !in_array(null, $oldIds, true) && !in_array(null, $ids, true)) {
407
            return;
408
        }
409
410 8
        $repository = _em()->getRepository($class);
411 8
        $objects = $repository->findBy([
412 8
            'id' => $ids,
413 8
            'site' => $this->getSite(),
414 8
        ]);
415
416 8
        $collection->clear();
417 8
        foreach ($objects as $object) {
418 1
            $collection->add($object);
419
        }
420
    }
421
422
    /**
423
     * Get artists.
424
     */
425 11
    #[API\Field(type: 'Artist[]')]
426
    public function getArtists(): DoctrineCollection
427
    {
428 11
        return $this->artists;
429
    }
430
431
    /**
432
     * Get antiqueNames.
433
     */
434
    #[API\Field(type: 'AntiqueName[]')]
435
    public function getAntiqueNames(): DoctrineCollection
436
    {
437
        return $this->antiqueNames;
438
    }
439
440
    /**
441
     * Add tag.
442
     */
443 1
    public function addTag(Tag $tag): void
444
    {
445 1
        if (!$this->tags->contains($tag)) {
446 1
            $this->tags[] = $tag;
447
        }
448 1
        $this->addEntireHierarchy($this->tags);
449
    }
450
451
    /**
452
     * Remove tag.
453
     */
454 1
    public function removeTag(Tag $tag): void
455
    {
456 1
        $this->tags->removeElement($tag);
457 1
        $this->addEntireHierarchy($this->tags);
458
    }
459
460
    /**
461
     * Get tags.
462
     */
463 1
    #[API\Field(type: 'Tag[]')]
464
    public function getTags(): DoctrineCollection
465
    {
466 1
        return $this->tags;
467
    }
468
469
    /**
470
     * The original card if this is a suggestion.
471
     */
472 4
    public function getOriginal(): ?self
473
    {
474 4
        return $this->original;
475
    }
476
477
    /**
478
     * Defines this card as suggestion for the $original.
479
     */
480 1
    public function setOriginal(?self $original): void
481
    {
482 1
        $this->original = $original;
483
    }
484
485 2
    public function getDocumentType(): ?DocumentType
486
    {
487 2
        return $this->documentType;
488
    }
489
490 4
    public function setDocumentType(?DocumentType $documentType): void
491
    {
492 4
        $this->documentType = $documentType;
493
    }
494
495
    /**
496
     * Get domains.
497
     */
498 3
    #[API\Field(type: 'Domain[]')]
499
    public function getDomains(): DoctrineCollection
500
    {
501 3
        return $this->domains;
502
    }
503
504
    /**
505
     * Add Domain.
506
     */
507 1
    public function addDomain(Domain $domain): void
508
    {
509 1
        if (!$this->domains->contains($domain)) {
510 1
            $this->domains[] = $domain;
511
        }
512
    }
513
514
    /**
515
     * Get periods.
516
     */
517 3
    #[API\Field(type: 'Period[]')]
518
    public function getPeriods(): DoctrineCollection
519
    {
520 3
        return $this->periods;
521
    }
522
523
    /**
524
     * Add Period.
525
     */
526 4
    public function addPeriod(Period $period): void
527
    {
528 4
        if (!$this->periods->contains($period)) {
529 4
            $this->periods[] = $period;
530
        }
531
    }
532
533
    /**
534
     * Remove Period.
535
     */
536
    public function removePeriod(Period $period): void
537
    {
538
        $this->periods->removeElement($period);
539
    }
540
541
    /**
542
     * Get materials.
543
     */
544 1
    #[API\Field(type: 'Material[]')]
545
    public function getMaterials(): DoctrineCollection
546
    {
547 1
        return $this->materials;
548
    }
549
550
    /**
551
     * Add Material.
552
     */
553 4
    public function addMaterial(Material $material): void
554
    {
555 4
        if (!$this->materials->contains($material)) {
556 4
            $this->materials[] = $material;
557
        }
558
559 4
        $this->addEntireHierarchy($this->materials);
560
    }
561
562
    /**
563
     * Remove Material.
564
     */
565
    public function removeMaterial(Material $material): void
566
    {
567
        $this->materials->removeElement($material);
568
        $this->addEntireHierarchy($this->materials);
569
    }
570
571
    /**
572
     * Add this card into the given collection.
573
     */
574 6
    public function addCollection(Collection $collection): void
575
    {
576 6
        if (!$this->collections->contains($collection)) {
577 6
            $this->collections->add($collection);
578
        }
579
580
        // If we are new and don't have a code yet, set one automatically
581 6
        if (!$this->getId() && !$this->getCode() && $this->canUpdateCode()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getId() of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
582
            /** @var CardRepository $userRepository */
583 1
            $userRepository = _em()->getRepository(self::class);
584 1
            $code = $userRepository->getNextCodeAvailable($collection);
585 1
            $this->setCode($code);
586
        }
587
    }
588
589
    /**
590
     * Remove this card from given collection.
591
     */
592 2
    public function removeCollection(Collection $collection): void
593
    {
594 2
        $this->collections->removeElement($collection);
595
    }
596
597
    /**
598
     * Notify the Card that a Dating was added.
599
     * This should only be called by Dating::setCard().
600
     */
601 4
    public function datingAdded(Dating $dating): void
602
    {
603 4
        $this->datings->add($dating);
604
    }
605
606
    /**
607
     * Notify the Card that a Dating was removed.
608
     * This should only be called by Dating::setCard().
609
     */
610 1
    public function datingRemoved(Dating $dating): void
611
    {
612 1
        $this->datings->removeElement($dating);
613
    }
614
615
    /**
616
     * Get image width.
617
     */
618 7
    public function getWidth(): int
619
    {
620 7
        return $this->width;
621
    }
622
623
    /**
624
     * Set image width.
625
     */
626 11
    #[API\Exclude]
627
    public function setWidth(int $width): void
628
    {
629 11
        $this->width = $width;
630
    }
631
632
    /**
633
     * Get image height.
634
     */
635 8
    public function getHeight(): int
636
    {
637 8
        return $this->height;
638
    }
639
640
    /**
641
     * Set image height.
642
     */
643 11
    #[API\Exclude]
644
    public function setHeight(int $height): void
645
    {
646 11
        $this->height = $height;
647
    }
648
649
    /**
650
     * Set the image file.
651
     */
652 9
    #[API\Input(type: '?GraphQL\Upload\UploadType')]
653
    public function setFile(UploadedFileInterface $file): void
654
    {
655 9
        $this->traitSetFile($file);
656
657
        try {
658 9
            $this->readFileInfo();
659
        } catch (Throwable $e) {
660
            throw new FileException($file, $e);
661
        }
662
    }
663
664
    /**
665
     * Get legacy id.
666
     */
667
    public function getLegacyId(): ?int
668
    {
669
        return $this->legacyId;
670
    }
671
672
    /**
673
     * Set legacy id.
674
     */
675
    #[API\Exclude]
676
    public function setLegacyId(int $legacyId): void
677
    {
678
        $this->legacyId = $legacyId;
679
    }
680
681
    /**
682
     * Read dimension and size from file on disk.
683
     */
684 9
    private function readFileInfo(): void
685
    {
686
        global $container;
687 9
        $path = $this->getPath();
688
689
        /** @var ImagineInterface $imagine */
690 9
        $imagine = $container->get(ImagineInterface::class);
691 9
        $image = $imagine->open($path);
692
693
        // Auto-rotate image if EXIF says it's rotated
694 9
        $autorotate = new Autorotate();
695 9
        $autorotate->apply($image);
696 9
        $image->save($path);
697
698 9
        $size = $image->getSize();
699
700 9
        $this->setWidth($size->getWidth());
701 9
        $this->setHeight($size->getHeight());
702 9
        $this->setFileSize(filesize($path));
703
    }
704
705 12
    private function computeDatings(): void
706
    {
707 12
        $rule = new DatingRule();
708
709
        // Delete all existing
710 12
        foreach ($this->datings as $d) {
711 2
            _em()->remove($d);
712
        }
713 12
        $this->datings->clear();
714
715
        // Add new one
716 12
        $datings = $rule->compute($this->dating);
717 12
        foreach ($datings as $d) {
718 3
            _em()->persist($d);
719 3
            $d->setCard($this);
720
        }
721
    }
722
723
    /**
724
     * Copy most of this card data into the given card.
725
     */
726 3
    public function copyInto(self $original): void
727
    {
728
        // Trigger loading of proxy
729 3
        $original->getName();
730
731 3
        $blacklist = [
732 3
            'id',
733 3
            'visibility',
734 3
            'code',
735 3
            '__initializer__',
736 3
            '__cloner__',
737 3
            '__isInitialized__',
738 3
        ];
739
740 3
        if (!$this->hasImage()) {
741 1
            $blacklist[] = 'filename';
742 1
            $blacklist[] = 'width';
743 1
            $blacklist[] = 'height';
744 1
            $blacklist[] = 'fileSize';
745
        }
746
747
        // Copy scalars
748 3
        foreach ($this as $property => $value) {
749 3
            if (in_array($property, $blacklist, true)) {
750 3
                continue;
751
            }
752
753 3
            if (is_scalar($value) || $value === null) {
754 3
                $original->$property = $value;
755
            }
756
        }
757
758
        // Copy a few collection and entities
759 3
        $original->artists = clone $this->artists;
760 3
        $original->tags = clone $this->tags;
761 3
        $original->materials = clone $this->materials;
762 3
        $original->domains = clone $this->domains;
763 3
        $original->periods = clone $this->periods;
764 3
        $original->computeDatings();
765 3
        $original->institution = $this->institution;
766 3
        $original->country = $this->country;
767 3
        $original->documentType = $this->documentType;
768
769
        // Copy file on disk
770 3
        if ($this->filename) {
771 2
            $original->generateUniqueFilename($this->filename);
772 2
            copy($this->getPath(), $original->getPath());
773
        }
774
    }
775
776
    /**
777
     * Get related cards.
778
     */
779 2
    #[API\Field(type: 'Card[]')]
780
    public function getCards(): DoctrineCollection
781
    {
782 2
        return $this->cards;
783
    }
784
785
    /**
786
     * Add related card.
787
     */
788 3
    public function addCard(self $card): void
789
    {
790 3
        if ($card === $this) {
791 1
            throw new InvalidArgumentException('A card cannot be related to itself');
792
        }
793
794 2
        if (!$this->cards->contains($card)) {
795 2
            $this->cards[] = $card;
796
        }
797
798 2
        if (!$card->getCards()->contains($this)) {
799 2
            $card->getCards()->add($this);
800
        }
801
    }
802
803
    /**
804
     * Remove related card.
805
     */
806 1
    public function removeCard(self $card): void
807
    {
808 1
        $this->cards->removeElement($card);
809 1
        $card->getCards()->removeElement($this);
810
    }
811
812
    /**
813
     * Return the change this card is a suggestion for, if any.
814
     */
815 4
    public function getChange(): ?Change
816
    {
817 4
        return $this->changes->first() ?: null;
818
    }
819
820
    /**
821
     * Notify the Card that it was added to a Change.
822
     * This should only be called by Change::addCard().
823
     */
824 4
    public function changeAdded(?Change $change): void
825
    {
826 4
        $this->changes->clear();
827 4
        if ($change) {
828 4
            $this->changes->add($change);
829
        }
830
    }
831
832
    /**
833
     * Set documentSize.
834
     */
835 7
    public function setDocumentSize(string $documentSize): void
836
    {
837 7
        $this->documentSize = $documentSize;
838
    }
839
840
    /**
841
     * Get documentSize.
842
     */
843
    public function getDocumentSize(): string
844
    {
845
        return $this->documentSize;
846
    }
847
848 8
    public function setIsbn(string $isbn): void
849
    {
850
        // Field is readonly and can only be emptied (Dilps only).
851 8
        if ($this->getSite() === SiteType::DILPS && $isbn !== '') {
852 8
            return;
853
        }
854
855
        $this->isbn = $isbn;
856
    }
857
858
    /**
859
     * Ensure that the entire hierarchy is added, but also make sure that
860
     * a non-leaf tag is added without one of his leaf.
861
     */
862 12
    private function addEntireHierarchy(DoctrineCollection $collection): void
863
    {
864 12
        $objects = $collection->toArray();
865 12
        $collection->clear();
866
867
        /** @var HasParentInterface $object */
868 12
        foreach ($objects as $object) {
869 5
            if ($object->hasChildren()) {
870 1
                continue;
871
            }
872
873 5
            $collection->add($object);
874
875 5
            foreach ($object->getParentHierarchy() as $parent) {
876 2
                if (!$collection->contains($parent)) {
877 2
                    $collection->add($parent);
878
                }
879
            }
880
        }
881
    }
882
}
883