Passed
Push — develop ( bffe25...680bb1 )
by Sam
15:16
created

Card::getShowHistoric()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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