Card::computeDatings()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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

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