Failed Conditions
Push — develop ( 56a035...e7ee0e )
by David
04:16
created

Card::autorotate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6.4624

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 19
ccs 3
cts 11
cp 0.2727
rs 9.9332
cc 3
nc 3
nop 1
crap 6.4624
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: 'CardVisibility', 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
    /**
199
     * Constructor.
200
     */
201 48
    public function __construct(string $name = '')
202
    {
203 48
        $this->setName($name);
204
205 48
        $this->changes = new ArrayCollection();
206 48
        $this->collections = new ArrayCollection();
207 48
        $this->artists = new ArrayCollection();
208 48
        $this->antiqueNames = new ArrayCollection();
209 48
        $this->tags = new ArrayCollection();
210 48
        $this->datings = new ArrayCollection();
211 48
        $this->cards = new ArrayCollection();
212 48
        $this->domains = new ArrayCollection();
213 48
        $this->periods = new ArrayCollection();
214 48
        $this->materials = new ArrayCollection();
215
    }
216
217
    /**
218
     * Return whether this is publicly available to everybody, or only member, or only owner.
219
     */
220 17
    public function getVisibility(): CardVisibility
221
    {
222 17
        return $this->visibility;
223
    }
224
225
    /**
226
     * Set whether this is publicly available to everybody, or only member, or only owner.
227
     */
228 27
    public function setVisibility(CardVisibility $visibility): void
229
    {
230 27
        if ($this->visibility === $visibility) {
231 11
            return;
232
        }
233
234 24
        $user = User::getCurrent();
235 24
        if ($visibility === CardVisibility::Public && $user->getRole() !== User::ROLE_ADMINISTRATOR) {
236 2
            throw new Exception('Only administrator can make a card public');
237
        }
238
239 23
        $this->visibility = $visibility;
240
    }
241
242
    /**
243
     * Get collections this card belongs to.
244
     */
245 10
    public function getCollections(): DoctrineCollection
246
    {
247 10
        return $this->collections;
248
    }
249
250
    /**
251
     * Get the card dating.
252
     *
253
     * This is a free form string that will be parsed to **try** and extract
254
     * some actual date range of dates. Any string is valid, but some parseable
255
     * values would typically be:
256
     *
257
     * - (1620-1652)
258
     * - 01.05.1917
259
     * - XIIIe siècle
260
     * - 1927
261
     * - c. 1100
262
     * - Fin du XIIe siècle
263
     */
264 5
    public function getDating(): string
265
    {
266 5
        return $this->dating;
267
    }
268
269
    /**
270
     * Set the card dating.
271
     *
272
     * This is a free form string that will be parsed to **try** and extract
273
     * some actual date range of dates. Any string is valid, but some parseable
274
     * values would typically be:
275
     *
276
     * - (1620-1652)
277
     * - 01.05.1917
278
     * - XIIIe siècle
279
     * - 1927
280
     * - c. 1100
281
     * - Fin du XIIe siècle
282
     */
283 10
    public function setDating(string $dating): void
284
    {
285 10
        if ($dating === $this->dating) {
286 1
            return;
287
        }
288 10
        $this->dating = $dating;
289
290 10
        $this->computeDatings();
291
    }
292
293
    /**
294
     * Return the automatically computed dating periods.
295
     */
296 4
    public function getDatings(): DoctrineCollection
297
    {
298 4
        return $this->datings;
299
    }
300
301
    /**
302
     * Set all artists at once by their names.
303
     *
304
     * Non-existing artists will be created automatically.
305
     *
306
     * @param null|string[] $artistNames
307
     */
308 9
    public function setArtists(?array $artistNames): void
309
    {
310 9
        if (null === $artistNames) {
0 ignored issues
show
introduced by
The condition null === $artistNames is always false.
Loading history...
311
            return;
312
        }
313
314 9
        $artistRepository = _em()->getRepository(Artist::class);
315 9
        $newArtists = $artistRepository->getOrCreateByNames($artistNames, $this->getSite());
316
317 9
        $oldIds = Utility::modelToId($this->artists->toArray());
318 9
        sort($oldIds);
319
320 9
        $newIds = Utility::modelToId($newArtists);
321 9
        sort($newIds);
322
323 9
        if ($oldIds === $newIds && !in_array(null, $oldIds, true) && !in_array(null, $newIds, true)) {
324
            return;
325
        }
326
327 9
        $this->artists->clear();
328 9
        foreach ($newArtists as $a) {
329 9
            $this->artists->add($a);
330
        }
331
    }
332
333
    /**
334
     * Set all materials at once.
335
     *
336
     * @param null|string[] $materials
337
     */
338 7
    #[API\Input(type: '?ID[]')]
339
    public function setMaterials(?array $materials): void
340
    {
341 7
        if (null === $materials) {
0 ignored issues
show
introduced by
The condition null === $materials is always false.
Loading history...
342
            return;
343
        }
344
345 7
        $this->setEntireCollection($materials, $this->materials, Material::class);
346 7
        $this->addEntireHierarchy($this->materials);
347
    }
348
349
    /**
350
     * Set all antiqueNames at once.
351
     *
352
     * @param null|string[] $antiqueNames
353
     */
354 8
    #[API\Input(type: '?ID[]')]
355
    public function setAntiqueNames(?array $antiqueNames): void
356
    {
357 8
        if (null === $antiqueNames) {
0 ignored issues
show
introduced by
The condition null === $antiqueNames is always false.
Loading history...
358
            return;
359
        }
360
361 8
        $this->setEntireCollection($antiqueNames, $this->antiqueNames, AntiqueName::class);
362
    }
363
364
    /**
365
     * Set all domains at once.
366
     *
367
     * @param null|string[] $domains
368
     */
369
    #[API\Input(type: '?ID[]')]
370
    public function setDomains(?array $domains): void
371
    {
372
        if (null === $domains) {
0 ignored issues
show
introduced by
The condition null === $domains is always false.
Loading history...
373
            return;
374
        }
375
376
        $this->setEntireCollection($domains, $this->domains, Domain::class);
377
    }
378
379
    /**
380
     * Set all periods at once.
381
     *
382
     * @param null|string[] $periods
383
     */
384 7
    #[API\Input(type: '?ID[]')]
385
    public function setPeriods(?array $periods): void
386
    {
387 7
        if (null === $periods) {
0 ignored issues
show
introduced by
The condition null === $periods is always false.
Loading history...
388
            return;
389
        }
390
391 7
        $this->setEntireCollection($periods, $this->periods, Period::class);
392
    }
393
394
    /**
395
     * Set all tags at once.
396
     *
397
     * @param null|string[] $tags
398
     */
399 8
    #[API\Input(type: '?ID[]')]
400
    public function setTags(?array $tags): void
401
    {
402 8
        if (null === $tags) {
0 ignored issues
show
introduced by
The condition null === $tags is always false.
Loading history...
403
            return;
404
        }
405
406 8
        $this->setEntireCollection($tags, $this->tags, Tag::class);
407 8
        $this->addEntireHierarchy($this->tags);
408
    }
409
410 9
    private function setEntireCollection(array $ids, DoctrineCollection $collection, string $class): void
411
    {
412 9
        $oldIds = Utility::modelToId($collection->toArray());
413 9
        sort($oldIds);
414
415 9
        sort($ids);
416
417 9
        if ($oldIds === $ids && !in_array(null, $oldIds, true) && !in_array(null, $ids, true)) {
418
            return;
419
        }
420
421 9
        $repository = _em()->getRepository($class);
422 9
        $objects = $repository->findBy([
423 9
            'id' => $ids,
424 9
            'site' => $this->getSite(),
425 9
        ]);
426
427 9
        $collection->clear();
428 9
        foreach ($objects as $object) {
429 1
            $collection->add($object);
430
        }
431
    }
432
433
    /**
434
     * Get artists.
435
     */
436 11
    public function getArtists(): DoctrineCollection
437
    {
438 11
        return $this->artists;
439
    }
440
441
    /**
442
     * Get antiqueNames.
443
     */
444 1
    public function getAntiqueNames(): DoctrineCollection
445
    {
446 1
        return $this->antiqueNames;
447
    }
448
449
    /**
450
     * Add tag.
451
     */
452 1
    public function addTag(Tag $tag): void
453
    {
454 1
        if (!$this->tags->contains($tag)) {
455 1
            $this->tags[] = $tag;
456
        }
457 1
        $this->addEntireHierarchy($this->tags);
458
    }
459
460
    /**
461
     * Remove tag.
462
     */
463 1
    public function removeTag(Tag $tag): void
464
    {
465 1
        $this->tags->removeElement($tag);
466 1
        $this->addEntireHierarchy($this->tags);
467
    }
468
469
    /**
470
     * Get tags.
471
     */
472 1
    public function getTags(): DoctrineCollection
473
    {
474 1
        return $this->tags;
475
    }
476
477
    /**
478
     * The original card if this is a suggestion.
479
     */
480 4
    public function getOriginal(): ?self
481
    {
482 4
        return $this->original;
483
    }
484
485
    /**
486
     * Defines this card as suggestion for the $original.
487
     */
488 1
    public function setOriginal(?self $original): void
489
    {
490 1
        $this->original = $original;
491
    }
492
493 2
    public function getDocumentType(): ?DocumentType
494
    {
495 2
        return $this->documentType;
496
    }
497
498 4
    public function setDocumentType(?DocumentType $documentType): void
499
    {
500 4
        $this->documentType = $documentType;
501
    }
502
503
    /**
504
     * Get domains.
505
     */
506 5
    public function getDomains(): DoctrineCollection
507
    {
508 5
        return $this->domains;
509
    }
510
511
    /**
512
     * Add Domain.
513
     */
514 1
    public function addDomain(Domain $domain): void
515
    {
516 1
        if (!$this->domains->contains($domain)) {
517 1
            $this->domains[] = $domain;
518
        }
519
    }
520
521
    /**
522
     * Get periods.
523
     */
524 3
    public function getPeriods(): DoctrineCollection
525
    {
526 3
        return $this->periods;
527
    }
528
529
    /**
530
     * Add Period.
531
     */
532 4
    public function addPeriod(Period $period): void
533
    {
534 4
        if (!$this->periods->contains($period)) {
535 4
            $this->periods[] = $period;
536
        }
537
    }
538
539
    /**
540
     * Remove Period.
541
     */
542
    public function removePeriod(Period $period): void
543
    {
544
        $this->periods->removeElement($period);
545
    }
546
547
    /**
548
     * Get materials.
549
     */
550 1
    public function getMaterials(): DoctrineCollection
551
    {
552 1
        return $this->materials;
553
    }
554
555
    /**
556
     * Add Material.
557
     */
558 4
    public function addMaterial(Material $material): void
559
    {
560 4
        if (!$this->materials->contains($material)) {
561 4
            $this->materials[] = $material;
562
        }
563
564 4
        $this->addEntireHierarchy($this->materials);
565
    }
566
567
    /**
568
     * Remove Material.
569
     */
570
    public function removeMaterial(Material $material): void
571
    {
572
        $this->materials->removeElement($material);
573
        $this->addEntireHierarchy($this->materials);
574
    }
575
576
    /**
577
     * Add this card into the given collection.
578
     */
579 6
    public function addCollection(Collection $collection): void
580
    {
581 6
        if (!$this->collections->contains($collection)) {
582 6
            $this->collections->add($collection);
583
        }
584
585
        // If we are new and don't have a code yet, set one automatically
586 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...
587
            /** @var CardRepository $userRepository */
588 1
            $userRepository = _em()->getRepository(self::class);
589 1
            $code = $userRepository->getNextCodeAvailable($collection);
590 1
            $this->setCode($code);
591
        }
592
    }
593
594
    /**
595
     * Remove this card from given collection.
596
     */
597 2
    public function removeCollection(Collection $collection): void
598
    {
599 2
        $this->collections->removeElement($collection);
600
    }
601
602
    /**
603
     * Notify the Card that a Dating was added.
604
     * This should only be called by Dating::setCard().
605
     */
606 4
    public function datingAdded(Dating $dating): void
607
    {
608 4
        $this->datings->add($dating);
609
    }
610
611
    /**
612
     * Notify the Card that a Dating was removed.
613
     * This should only be called by Dating::setCard().
614
     */
615 1
    public function datingRemoved(Dating $dating): void
616
    {
617 1
        $this->datings->removeElement($dating);
618
    }
619
620
    /**
621
     * Get image width.
622
     */
623 7
    public function getWidth(): int
624
    {
625 7
        return $this->width;
626
    }
627
628
    /**
629
     * Set image width.
630
     */
631 11
    #[API\Exclude]
632
    public function setWidth(int $width): void
633
    {
634 11
        $this->width = $width;
635
    }
636
637
    /**
638
     * Get image height.
639
     */
640 12
    public function getHeight(): int
641
    {
642 12
        return $this->height;
643
    }
644
645
    /**
646
     * Set image height.
647
     */
648 11
    #[API\Exclude]
649
    public function setHeight(int $height): void
650
    {
651 11
        $this->height = $height;
652
    }
653
654
    /**
655
     * Set the image file.
656
     */
657 9
    #[API\Input(type: '?GraphQL\Upload\UploadType')]
658
    public function setFile(UploadedFileInterface $file): void
659
    {
660
        global $container;
661
662 9
        $this->traitSetFile($file);
663
664
        try {
665
            /** @var ImagineInterface $imagine */
666 9
            $imagine = $container->get(ImagineInterface::class);
667 9
            $image = $imagine->open($this->getPath());
668
669 9
            $this->autorotate($image);
670 9
            $this->readFileInfo($image);
671
        } catch (Throwable $e) {
672
            throw new FileException($file, $e);
673
        }
674
675
        // Create most used thumbnails.
676 9
        $imageResizer = $container->get(ImageResizer::class);
677 9
        foreach ([300, 2000] as $maxHeight) {
678 9
            $imageResizer->resize($this, $maxHeight, true);
679
        }
680
    }
681
682
    /**
683
     * Get legacy id.
684
     */
685
    public function getLegacyId(): ?int
686
    {
687
        return $this->legacyId;
688
    }
689
690
    /**
691
     * Set legacy id.
692
     */
693
    #[API\Exclude]
694
    public function setLegacyId(int $legacyId): void
695
    {
696
        $this->legacyId = $legacyId;
697
    }
698
699
    /**
700
     * Try to auto-rotate image if EXIF says it's rotated.
701
     * If the size of the resulting file exceed the autorized upload filesize
702
     * configured for the server (php's upload_max_filesize), do nothing.
703
     *
704
     * More informations about EXIF orientation here:
705
     * https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
706
     */
707 9
    private function autorotate(ImageInterface $image): void
708
    {
709 9
        $autorotate = new Autorotate();
710
711
        // Check if the image is EXIF oriented.
712 9
        if (!empty($autorotate->getTransformations($image))) {
713
            $autorotate->apply($image);
714
715
            // Save the rotate image to a temporary file to check its size.
716
            $tempFile = tempnam('data/tmp/', 'rotated-image');
717
            $image->save($tempFile);
718
            $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

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