Card::addEntireHierarchy()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 16
c 0
b 0
f 0
ccs 10
cts 10
cp 1
rs 9.6111
cc 5
nc 5
nop 1
crap 5
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\CardYearRangeOperatorType;
9
use Application\Api\Input\Operator\DatingYearRangeOperatorType;
10
use Application\Api\Input\Operator\LocalityOrInstitutionLocalityOperatorType;
11
use Application\Api\Input\Operator\LocationOperatorType;
12
use Application\Api\Input\Operator\NameOrExpandedNameOperatorType;
13
use Application\Api\Input\Sorting\Artists;
14
use Application\Api\Input\Sorting\Domains;
15
use Application\Api\Input\Sorting\InstitutionLocality;
16
use Application\Enum\CardVisibility;
17
use Application\Enum\Site;
18
use Application\FriendlyException;
19
use Application\Repository\CardRepository;
20
use Application\Service\DatingRule;
21
use Application\Service\ImageResizer;
22
use Application\Traits\CardSimpleProperties;
23
use Application\Traits\HasAddress;
24
use Application\Traits\HasCode;
25
use Application\Traits\HasFileSize;
26
use Application\Traits\HasImage;
27
use Application\Traits\HasInstitution;
28
use Application\Traits\HasParentInterface;
29
use Application\Traits\HasRichTextName;
30
use Application\Traits\HasSite;
31
use Application\Traits\HasSiteInterface;
32
use Application\Traits\HasYearRange;
33
use Doctrine\Common\Collections\ArrayCollection;
34
use Doctrine\Common\Collections\Collection as DoctrineCollection;
35
use Doctrine\ORM\Mapping as ORM;
36
use Ecodev\Felix\Api\Exception;
37
use Ecodev\Felix\Model\Image;
38
use Ecodev\Felix\Utility;
39
use GraphQL\Doctrine\Attribute as API;
40
use Imagine\Filter\Basic\Autorotate;
41
use Imagine\Image\ImageInterface;
42
use Imagine\Image\ImagineInterface;
43
use InvalidArgumentException;
44
use Psr\Http\Message\UploadedFileInterface;
45
use Throwable;
46
47
/**
48
 * A card containing an image and some information about it.
49
 */
50
#[ORM\Index(name: 'card_name_idx', columns: ['name'])]
51
#[ORM\Index(name: 'card_plain_name_idx', columns: ['plain_name'])]
52
#[ORM\Index(name: 'card_locality_idx', columns: ['locality'])]
53
#[ORM\Index(name: 'card_area_idx', columns: ['area'])]
54
#[ORM\Index(name: 'FULLTEXT__CARD_CUSTOM_SEARCH', flags: ['fulltext'], fields: [
55
    'dating',
56
    'cachedArtistNames',
57
    'addition',
58
    'expandedName',
59
    'material',
60
    'techniqueAuthor',
61
    'objectReference',
62
    'corpus',
63
    'street',
64
    'locality',
65
    'code',
66
    'name',
67
], )]
68
#[ORM\Index(name: 'FULLTEXT__CARD_LOCALITY', flags: ['fulltext'], fields: ['locality'])]
69
#[ORM\Index(name: 'FULLTEXT__CARD_NAMES', flags: ['fulltext'], fields: ['name', 'expandedName'])]
70
#[ORM\UniqueConstraint(name: 'unique_code', columns: ['code', 'site'])]
71
#[API\Filter(field: 'nameOrExpandedName', operator: NameOrExpandedNameOperatorType::class, type: 'string')]
72
#[API\Filter(field: 'localityOrInstitutionLocality', operator: LocalityOrInstitutionLocalityOperatorType::class, type: 'string')]
73
#[API\Filter(field: 'datingYearRange', operator: DatingYearRangeOperatorType::class, type: 'int')]
74
#[API\Filter(field: 'cardYearRange', operator: CardYearRangeOperatorType::class, type: 'int')]
75
#[API\Filter(field: 'custom', operator: LocationOperatorType::class, type: 'string')]
76
#[API\Sorting(Artists::class)]
77
#[API\Sorting(Domains::class)]
78
#[API\Sorting(InstitutionLocality::class)]
79
#[API\Sorting(\Application\Api\Input\Sorting\DocumentType::class)]
80
#[ORM\HasLifecycleCallbacks]
81
#[ORM\Entity(CardRepository::class)]
82
class Card extends AbstractModel implements HasSiteInterface, Image
83
{
84
    use CardSimpleProperties;
85
    use HasAddress;
86
    use HasCode;
87
    use HasFileSize;
88
    use HasImage {
89
        setFile as traitSetFile;
90
    }
91
    use HasInstitution;
92
    use HasRichTextName;
93
    use HasSite;
94
    use HasYearRange;
95
96
    private const IMAGE_PATH = 'data/images/';
97
98
    #[ORM\Column(type: 'enum', options: ['default' => CardVisibility::Private])]
99
    private CardVisibility $visibility = CardVisibility::Private;
100
101
    #[ORM\Column(type: 'integer')]
102
    private int $width = 0;
103
104
    #[ORM\Column(type: 'integer')]
105
    private int $height = 0;
106
107
    #[ORM\Column(type: 'string', options: ['default' => ''])]
108
    private string $dating = '';
109
110
    /**
111
     * This is a form of cache of all artist names whose only purpose is to be able
112
     * to search on artists more easily. It is automatically maintained via DB triggers.
113
     */
114
    #[API\Exclude]
115
    #[ORM\Column(type: 'text', options: ['default' => ''])]
116
    private string $cachedArtistNames = '';
117
118
    /**
119
     * @var DoctrineCollection<Collection>
120
     */
121
    #[ORM\ManyToMany(targetEntity: Collection::class)]
122
    private DoctrineCollection $collections;
123
124
    /**
125
     * @var DoctrineCollection<Artist>
126
     */
127
    #[ORM\ManyToMany(targetEntity: Artist::class)]
128
    private DoctrineCollection $artists;
129
130
    /**
131
     * @var DoctrineCollection<AntiqueName>
132
     */
133
    #[ORM\ManyToMany(targetEntity: AntiqueName::class)]
134
    private DoctrineCollection $antiqueNames;
135
136
    /**
137
     * @var DoctrineCollection<Tag>
138
     */
139
    #[ORM\ManyToMany(targetEntity: Tag::class)]
140
    private DoctrineCollection $tags;
141
142
    /**
143
     * @var DoctrineCollection<Dating>
144
     */
145
    #[ORM\OneToMany(targetEntity: Dating::class, mappedBy: 'card')]
146
    private DoctrineCollection $datings;
147
148
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
149
    #[ORM\ManyToOne(targetEntity: self::class)]
150
    private ?Card $original = null;
151
152
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
153
    #[ORM\ManyToOne(targetEntity: DocumentType::class)]
154
    private ?DocumentType $documentType = null;
155
156
    /**
157
     * @var DoctrineCollection<Domain>
158
     */
159
    #[ORM\ManyToMany(targetEntity: Domain::class)]
160
    private DoctrineCollection $domains;
161
162
    /**
163
     * @var DoctrineCollection<Period>
164
     */
165
    #[ORM\ManyToMany(targetEntity: Period::class)]
166
    private DoctrineCollection $periods;
167
168
    /**
169
     * @var DoctrineCollection<Material>
170
     */
171
    #[ORM\ManyToMany(targetEntity: Material::class)]
172
    private DoctrineCollection $materials;
173
174
    /**
175
     * @var DoctrineCollection<Card>
176
     */
177
    #[ORM\ManyToMany(targetEntity: self::class)]
178
    private DoctrineCollection $cards;
179
180
    /**
181
     * There is actually 0 to 1 change, never more. And this is
182
     * enforced by DB unique constraints on the mapping side.
183
     *
184
     * @var DoctrineCollection<Change>
185
     */
186
    #[ORM\OneToMany(targetEntity: Change::class, mappedBy: 'suggestion')]
187
    private DoctrineCollection $changes;
188
189
    #[ORM\Column(type: 'string', length: 191, options: ['default' => ''])]
190
    private string $documentSize = '';
191
192
    #[ORM\Column(name: 'legacy_id', type: 'integer', nullable: true)]
193
    private ?int $legacyId = null;
194
195 48
    public function __construct(string $name = '')
196
    {
197 48
        $this->setName($name);
198
199 48
        $this->changes = new ArrayCollection();
200 48
        $this->collections = new ArrayCollection();
201 48
        $this->artists = new ArrayCollection();
202 48
        $this->antiqueNames = new ArrayCollection();
203 48
        $this->tags = new ArrayCollection();
204 48
        $this->datings = new ArrayCollection();
205 48
        $this->cards = new ArrayCollection();
206 48
        $this->domains = new ArrayCollection();
207 48
        $this->periods = new ArrayCollection();
208 48
        $this->materials = new ArrayCollection();
209
    }
210
211
    /**
212
     * Return whether this is publicly available to everybody, or only member, or only owner.
213
     */
214 17
    public function getVisibility(): CardVisibility
215
    {
216 17
        return $this->visibility;
217
    }
218
219
    /**
220
     * Set whether this is publicly available to everybody, or only member, or only owner.
221
     */
222 27
    public function setVisibility(CardVisibility $visibility): void
223
    {
224 27
        if ($this->visibility === $visibility) {
225 11
            return;
226
        }
227
228 24
        $user = User::getCurrent();
229 24
        if ($visibility === CardVisibility::Public && $user->getRole() !== User::ROLE_ADMINISTRATOR) {
230 2
            throw new Exception('Only administrator can make a card public');
231
        }
232
233 23
        $this->visibility = $visibility;
234
    }
235
236
    /**
237
     * Get collections this card belongs to.
238
     */
239 10
    public function getCollections(): DoctrineCollection
240
    {
241 10
        return $this->collections;
242
    }
243
244
    /**
245
     * Get the card dating.
246
     *
247
     * This is a free form string that will be parsed to **try** and extract
248
     * some actual date range of dates. Any string is valid, but some parseable
249
     * values would typically be:
250
     *
251
     * - (1620-1652)
252
     * - 01.05.1917
253
     * - XIIIe siècle
254
     * - 1927
255
     * - c. 1100
256
     * - Fin du XIIe siècle
257
     */
258 5
    public function getDating(): string
259
    {
260 5
        return $this->dating;
261
    }
262
263
    /**
264
     * Set the card dating.
265
     *
266
     * This is a free form string that will be parsed to **try** and extract
267
     * some actual date range of dates. Any string is valid, but some parseable
268
     * values would typically be:
269
     *
270
     * - (1620-1652)
271
     * - 01.05.1917
272
     * - XIIIe siècle
273
     * - 1927
274
     * - c. 1100
275
     * - Fin du XIIe siècle
276
     */
277 10
    public function setDating(string $dating): void
278
    {
279 10
        if ($dating === $this->dating) {
280 1
            return;
281
        }
282 10
        $this->dating = $dating;
283
284 10
        $this->computeDatings();
285
    }
286
287
    /**
288
     * Return the automatically computed dating periods.
289
     */
290 4
    public function getDatings(): DoctrineCollection
291
    {
292 4
        return $this->datings;
293
    }
294
295
    /**
296
     * Set all artists at once by their names.
297
     *
298
     * Non-existing artists will be created automatically.
299
     *
300
     * @param null|string[] $artistNames
301
     */
302 9
    public function setArtists(?array $artistNames): void
303
    {
304 9
        if (null === $artistNames) {
0 ignored issues
show
introduced by
The condition null === $artistNames is always false.
Loading history...
305
            return;
306
        }
307
308 9
        $artistRepository = _em()->getRepository(Artist::class);
309 9
        $newArtists = $artistRepository->getOrCreateByNames($artistNames, $this->getSite());
310
311 9
        $oldIds = Utility::modelToId($this->artists->toArray());
312 9
        sort($oldIds);
313
314 9
        $newIds = Utility::modelToId($newArtists);
315 9
        sort($newIds);
316
317 9
        if ($oldIds === $newIds && !in_array(null, $oldIds, true) && !in_array(null, $newIds, true)) {
318
            return;
319
        }
320
321 9
        $this->artists->clear();
322 9
        foreach ($newArtists as $a) {
323 9
            $this->artists->add($a);
324
        }
325
    }
326
327
    /**
328
     * Set all materials at once.
329
     *
330
     * @param null|string[] $materials
331
     */
332 7
    #[API\Input(type: '?ID[]')]
333
    public function setMaterials(?array $materials): void
334
    {
335 7
        if (null === $materials) {
0 ignored issues
show
introduced by
The condition null === $materials is always false.
Loading history...
336
            return;
337
        }
338
339 7
        $this->setEntireCollection($materials, $this->materials, Material::class);
340 7
        $this->addEntireHierarchy($this->materials);
341
    }
342
343
    /**
344
     * Set all antiqueNames at once.
345
     *
346
     * @param null|string[] $antiqueNames
347
     */
348 8
    #[API\Input(type: '?ID[]')]
349
    public function setAntiqueNames(?array $antiqueNames): void
350
    {
351 8
        if (null === $antiqueNames) {
0 ignored issues
show
introduced by
The condition null === $antiqueNames is always false.
Loading history...
352
            return;
353
        }
354
355 8
        $this->setEntireCollection($antiqueNames, $this->antiqueNames, AntiqueName::class);
356
    }
357
358
    /**
359
     * Set all domains at once.
360
     *
361
     * @param null|string[] $domains
362
     */
363
    #[API\Input(type: '?ID[]')]
364
    public function setDomains(?array $domains): void
365
    {
366
        if (null === $domains) {
0 ignored issues
show
introduced by
The condition null === $domains is always false.
Loading history...
367
            return;
368
        }
369
370
        $this->setEntireCollection($domains, $this->domains, Domain::class);
371
    }
372
373
    /**
374
     * Set all periods at once.
375
     *
376
     * @param null|string[] $periods
377
     */
378 7
    #[API\Input(type: '?ID[]')]
379
    public function setPeriods(?array $periods): void
380
    {
381 7
        if (null === $periods) {
0 ignored issues
show
introduced by
The condition null === $periods is always false.
Loading history...
382
            return;
383
        }
384
385 7
        $this->setEntireCollection($periods, $this->periods, Period::class);
386
    }
387
388
    /**
389
     * Set all tags at once.
390
     *
391
     * @param null|string[] $tags
392
     */
393 8
    #[API\Input(type: '?ID[]')]
394
    public function setTags(?array $tags): void
395
    {
396 8
        if (null === $tags) {
0 ignored issues
show
introduced by
The condition null === $tags is always false.
Loading history...
397
            return;
398
        }
399
400 8
        $this->setEntireCollection($tags, $this->tags, Tag::class);
401 8
        $this->addEntireHierarchy($this->tags);
402
    }
403
404 9
    private function setEntireCollection(array $ids, DoctrineCollection $collection, string $class): void
405
    {
406 9
        $oldIds = Utility::modelToId($collection->toArray());
407 9
        sort($oldIds);
408
409 9
        sort($ids);
410
411 9
        if ($oldIds === $ids && !in_array(null, $oldIds, true) && !in_array(null, $ids, true)) {
412
            return;
413
        }
414
415 9
        $repository = _em()->getRepository($class);
416 9
        $objects = $repository->findBy([
417 9
            'id' => $ids,
418 9
            'site' => $this->getSite(),
419 9
        ]);
420
421 9
        $collection->clear();
422 9
        foreach ($objects as $object) {
423 1
            $collection->add($object);
424
        }
425
    }
426
427
    /**
428
     * Get artists.
429
     */
430 11
    public function getArtists(): DoctrineCollection
431
    {
432 11
        return $this->artists;
433
    }
434
435
    /**
436
     * Get antiqueNames.
437
     */
438 1
    public function getAntiqueNames(): DoctrineCollection
439
    {
440 1
        return $this->antiqueNames;
441
    }
442
443
    /**
444
     * Add tag.
445
     */
446 1
    public function addTag(Tag $tag): void
447
    {
448 1
        if (!$this->tags->contains($tag)) {
449 1
            $this->tags[] = $tag;
450
        }
451 1
        $this->addEntireHierarchy($this->tags);
452
    }
453
454
    /**
455
     * Remove tag.
456
     */
457 1
    public function removeTag(Tag $tag): void
458
    {
459 1
        $this->tags->removeElement($tag);
460 1
        $this->addEntireHierarchy($this->tags);
461
    }
462
463
    /**
464
     * Get tags.
465
     */
466 1
    public function getTags(): DoctrineCollection
467
    {
468 1
        return $this->tags;
469
    }
470
471
    /**
472
     * The original card if this is a suggestion.
473
     */
474 4
    public function getOriginal(): ?self
475
    {
476 4
        return $this->original;
477
    }
478
479
    /**
480
     * Defines this card as suggestion for the $original.
481
     */
482 1
    public function setOriginal(?self $original): void
483
    {
484 1
        $this->original = $original;
485
    }
486
487 2
    public function getDocumentType(): ?DocumentType
488
    {
489 2
        return $this->documentType;
490
    }
491
492 4
    public function setDocumentType(?DocumentType $documentType): void
493
    {
494 4
        $this->documentType = $documentType;
495
    }
496
497
    /**
498
     * Get domains.
499
     */
500 5
    public function getDomains(): DoctrineCollection
501
    {
502 5
        return $this->domains;
503
    }
504
505
    /**
506
     * Add Domain.
507
     */
508 1
    public function addDomain(Domain $domain): void
509
    {
510 1
        if (!$this->domains->contains($domain)) {
511 1
            $this->domains[] = $domain;
512
        }
513
    }
514
515
    /**
516
     * Get periods.
517
     */
518 3
    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
    public function getMaterials(): DoctrineCollection
545
    {
546 1
        return $this->materials;
547
    }
548
549
    /**
550
     * Add Material.
551
     */
552 4
    public function addMaterial(Material $material): void
553
    {
554 4
        if (!$this->materials->contains($material)) {
555 4
            $this->materials[] = $material;
556
        }
557
558 4
        $this->addEntireHierarchy($this->materials);
559
    }
560
561
    /**
562
     * Remove Material.
563
     */
564
    public function removeMaterial(Material $material): void
565
    {
566
        $this->materials->removeElement($material);
567
        $this->addEntireHierarchy($this->materials);
568
    }
569
570
    /**
571
     * Add this card into the given collection.
572
     */
573 6
    public function addCollection(Collection $collection): void
574
    {
575 6
        if (!$this->collections->contains($collection)) {
576 6
            $this->collections->add($collection);
577
        }
578
579
        // If we are new and don't have a code yet, set one automatically
580 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...
581
            /** @var CardRepository $userRepository */
582 1
            $userRepository = _em()->getRepository(self::class);
583 1
            $code = $userRepository->getNextCodeAvailable($collection);
584 1
            $this->setCode($code);
585
        }
586
    }
587
588
    /**
589
     * Remove this card from given collection.
590
     */
591 2
    public function removeCollection(Collection $collection): void
592
    {
593 2
        $this->collections->removeElement($collection);
594
    }
595
596
    /**
597
     * Notify the Card that a Dating was added.
598
     * This should only be called by Dating::setCard().
599
     */
600 4
    public function datingAdded(Dating $dating): void
601
    {
602 4
        $this->datings->add($dating);
603
    }
604
605
    /**
606
     * Notify the Card that a Dating was removed.
607
     * This should only be called by Dating::setCard().
608
     */
609 1
    public function datingRemoved(Dating $dating): void
610
    {
611 1
        $this->datings->removeElement($dating);
612
    }
613
614
    /**
615
     * Get image width.
616
     */
617 7
    public function getWidth(): int
618
    {
619 7
        return $this->width;
620
    }
621
622
    /**
623
     * Set image width.
624
     */
625 11
    #[API\Exclude]
626
    public function setWidth(int $width): void
627
    {
628 11
        $this->width = $width;
629
    }
630
631
    /**
632
     * Get image height.
633
     */
634 12
    public function getHeight(): int
635
    {
636 12
        return $this->height;
637
    }
638
639
    /**
640
     * Set image height.
641
     */
642 11
    #[API\Exclude]
643
    public function setHeight(int $height): void
644
    {
645 11
        $this->height = $height;
646
    }
647
648
    /**
649
     * Set the image file.
650
     */
651 9
    #[API\Input(type: '?GraphQL\Upload\UploadType')]
652
    public function setFile(UploadedFileInterface $file): void
653
    {
654
        global $container;
655
656 9
        $this->traitSetFile($file);
657
658
        try {
659
            /** @var ImagineInterface $imagine */
660 9
            $imagine = $container->get(ImagineInterface::class);
661 9
            $image = FriendlyException::try(fn () => $imagine->open($this->getPath()));
662
663 9
            $this->autorotate($image);
664 9
            $this->readFileInfo($image);
665
        } catch (Throwable $e) {
666
            throw new FileException($file, $e);
667
        }
668
669
        // Create most used thumbnails.
670 9
        $imageResizer = $container->get(ImageResizer::class);
671 9
        foreach ([300, 2000] as $maxHeight) {
672 9
            $imageResizer->resize($this, $maxHeight, true);
673
        }
674
    }
675
676
    /**
677
     * Get legacy id.
678
     */
679
    public function getLegacyId(): ?int
680
    {
681
        return $this->legacyId;
682
    }
683
684
    /**
685
     * Set legacy id.
686
     */
687
    #[API\Exclude]
688
    public function setLegacyId(int $legacyId): void
689
    {
690
        $this->legacyId = $legacyId;
691
    }
692
693
    /**
694
     * Try to auto-rotate image if EXIF says it's rotated.
695
     * If the size of the resulting file exceed the autorized upload filesize
696
     * configured for the server (php's upload_max_filesize), do nothing.
697
     *
698
     * More informations about EXIF orientation here:
699
     * https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
700
     */
701 9
    private function autorotate(ImageInterface $image): void
702
    {
703 9
        $autorotate = new Autorotate();
704
705
        // Check if the image is EXIF oriented.
706 9
        if (!empty($autorotate->getTransformations($image))) {
707
            $autorotate->apply($image);
708
709
            // Save the rotate image to a temporary file to check its size.
710
            $tempFile = tempnam('data/tmp/', 'rotated-image');
711
            FriendlyException::try(fn () => $image->save($tempFile));
712
            $maxSize = ini_parse_quantity(ini_get('upload_max_filesize'));
0 ignored issues
show
Bug introduced by
The function ini_parse_quantity was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

712
            $maxSize = /** @scrutinizer ignore-call */ ini_parse_quantity(ini_get('upload_max_filesize'));
Loading history...
713
            $newSize = filesize($tempFile);
714
            unlink($tempFile);
715
716
            // We only rotate if the size of the rotated file do not exceed the
717
            // authorized upload filesize configured for the server.
718
            if ($newSize < $maxSize) {
719
                FriendlyException::try(fn () => $image->save($this->getPath()));
720
            }
721
        }
722
    }
723
724
    /**
725
     * Read dimension and size from file on disk.
726
     */
727 9
    private function readFileInfo(ImageInterface $image): void
728
    {
729
        // Ensure that we read fresh stats from disk.
730 9
        clearstatcache(true, $this->getPath());
731
732 9
        $size = $image->getSize();
733
734 9
        $this->setWidth($size->getWidth());
735 9
        $this->setHeight($size->getHeight());
736 9
        $this->setFileSize(filesize($this->getPath()));
737
    }
738
739 12
    private function computeDatings(): void
740
    {
741 12
        $rule = new DatingRule();
742
743
        // Delete all existing
744 12
        foreach ($this->datings as $d) {
745 2
            _em()->remove($d);
746
        }
747 12
        $this->datings->clear();
748
749
        // Add new one
750 12
        $datings = $rule->compute($this->dating);
751 12
        foreach ($datings as $d) {
752 3
            _em()->persist($d);
753 3
            $d->setCard($this);
754
        }
755
    }
756
757
    /**
758
     * Copy most of this card data into the given card.
759
     */
760 3
    public function copyInto(self $original): void
761
    {
762
        // Trigger loading of proxy
763 3
        $original->getName();
764
765 3
        $blacklist = [
766 3
            'id',
767 3
            'visibility',
768 3
            'code',
769 3
            '__initializer__',
770 3
            '__cloner__',
771 3
            '__isInitialized__',
772 3
        ];
773
774 3
        if (!$this->hasImage()) {
775 1
            $blacklist[] = 'filename';
776 1
            $blacklist[] = 'width';
777 1
            $blacklist[] = 'height';
778 1
            $blacklist[] = 'fileSize';
779
        }
780
781
        // Copy scalars
782 3
        foreach ($this as $property => $value) {
783 3
            if (in_array($property, $blacklist, true)) {
784 3
                continue;
785
            }
786
787 3
            if (is_scalar($value) || $value === null) {
788 3
                $original->$property = $value;
789
            }
790
        }
791
792
        // Copy a few collection and entities
793 3
        $original->artists = clone $this->artists;
794 3
        $original->tags = clone $this->tags;
795 3
        $original->materials = clone $this->materials;
796 3
        $original->domains = clone $this->domains;
797 3
        $original->periods = clone $this->periods;
798 3
        $original->computeDatings();
799 3
        $original->institution = $this->institution;
800 3
        $original->country = $this->country;
801 3
        $original->documentType = $this->documentType;
802
803
        // Copy file on disk
804 3
        if ($this->filename) {
805 2
            $original->generateUniqueFilename($this->filename);
806 2
            copy($this->getPath(), $original->getPath());
807
        }
808
    }
809
810
    /**
811
     * Get related cards.
812
     */
813 2
    public function getCards(): DoctrineCollection
814
    {
815 2
        return $this->cards;
816
    }
817
818
    /**
819
     * Add related card.
820
     */
821 3
    public function addCard(self $card): void
822
    {
823 3
        if ($card === $this) {
824 1
            throw new InvalidArgumentException('A card cannot be related to itself');
825
        }
826
827 2
        if (!$this->cards->contains($card)) {
828 2
            $this->cards[] = $card;
829
        }
830
831 2
        if (!$card->getCards()->contains($this)) {
832 2
            $card->getCards()->add($this);
833
        }
834
    }
835
836
    /**
837
     * Remove related card.
838
     */
839 1
    public function removeCard(self $card): void
840
    {
841 1
        $this->cards->removeElement($card);
842 1
        $card->getCards()->removeElement($this);
843
    }
844
845
    /**
846
     * Return the change this card is a suggestion for, if any.
847
     */
848 4
    public function getChange(): ?Change
849
    {
850 4
        return $this->changes->first() ?: null;
851
    }
852
853
    /**
854
     * Notify the Card that it was added to a Change.
855
     * This should only be called by Change::addCard().
856
     */
857 4
    public function changeAdded(?Change $change): void
858
    {
859 4
        $this->changes->clear();
860 4
        if ($change) {
861 4
            $this->changes->add($change);
862
        }
863
    }
864
865
    /**
866
     * Set documentSize.
867
     */
868 7
    public function setDocumentSize(string $documentSize): void
869
    {
870 7
        $this->documentSize = $documentSize;
871
    }
872
873
    /**
874
     * Get documentSize.
875
     */
876
    public function getDocumentSize(): string
877
    {
878
        return $this->documentSize;
879
    }
880
881 8
    public function setIsbn(string $isbn): void
882
    {
883
        // Field is readonly and can only be emptied (Dilps only).
884 8
        if ($this->getSite() === Site::Dilps && $isbn !== '') {
885 8
            return;
886
        }
887
888
        $this->isbn = $isbn;
889
    }
890
891
    /**
892
     * Ensure that the entire hierarchy is added, but also make sure that
893
     * a non-leaf tag is added without one of his leaf.
894
     */
895 12
    private function addEntireHierarchy(DoctrineCollection $collection): void
896
    {
897 12
        $objects = $collection->toArray();
898 12
        $collection->clear();
899
900
        /** @var HasParentInterface $object */
901 12
        foreach ($objects as $object) {
902 5
            if ($object->hasChildren()) {
903 1
                continue;
904
            }
905
906 5
            $collection->add($object);
907
908 5
            foreach ($object->getParentHierarchy() as $parent) {
909 2
                if (!$collection->contains($parent)) {
910 2
                    $collection->add($parent);
911
                }
912
            }
913
        }
914
    }
915
916
    /**
917
     * Return whether this card belongs to at least one historic collection.
918
     */
919
    public function getShowHistoric(): bool
920
    {
921
        return _em()->getRepository(self::class)->getAclFilter()->runWithoutAcl(function () {
922
            foreach ($this->collections as $collection) {
923
                if ($collection->isHistoric() && $collection->isSource()) {
924
                    return true;
925
                }
926
            }
927
928
            return false;
929
        });
930
    }
931
}
932