Failed Conditions
Push — master ( b1d4df...5a9cf3 )
by David
04:05
created

Card   F

Complexity

Total Complexity 101

Size/Duplication

Total Lines 844
Duplicated Lines 0 %

Test Coverage

Coverage 89.84%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 101
eloc 289
c 5
b 0
f 0
dl 0
loc 844
ccs 221
cts 246
cp 0.8984
rs 2

54 Methods

Rating   Name   Duplication   Size   Complexity  
A getArtists() 0 3 1
A addPeriod() 0 4 2
A getDocumentType() 0 3 1
A removeCard() 0 4 1
A getTags() 0 3 1
A addCollection() 0 12 5
A setEntireCollection() 0 20 5
A removeMaterial() 0 4 1
A setDating() 0 8 2
A setHeight() 0 4 1
A getDomains() 0 3 1
A setOriginal() 0 3 1
A datingAdded() 0 3 1
A getLegacyId() 0 3 1
A setTags() 0 9 2
A removeTag() 0 4 1
A getHeight() 0 3 1
A getVisibility() 0 3 1
A removeCollection() 0 3 1
A setFile() 0 17 3
A setVisibility() 0 12 4
A setDomains() 0 8 2
A datingRemoved() 0 3 1
A addMaterial() 0 7 2
A getCollections() 0 3 1
A __construct() 0 14 1
A addDomain() 0 4 2
A setWidth() 0 4 1
A getDating() 0 3 1
A getWidth() 0 3 1
A addTag() 0 6 2
A setPeriods() 0 8 2
A setDocumentType() 0 3 1
A getPeriods() 0 3 1
A getOriginal() 0 3 1
A setArtists() 0 22 6
A setLegacyId() 0 4 1
A getAntiqueNames() 0 3 1
A getMaterials() 0 3 1
A setAntiqueNames() 0 8 2
A setMaterials() 0 9 2
A getDatings() 0 3 1
A removePeriod() 0 3 1
A addCard() 0 12 4
A addEntireHierarchy() 0 16 5
A changeAdded() 0 5 2
A getCards() 0 3 1
A setDocumentSize() 0 3 1
A getChange() 0 3 2
A computeDatings() 0 15 3
B copyInto() 0 47 7
A readFileInfo() 0 22 1
A setIsbn() 0 8 3
A getDocumentSize() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Card often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Card, and based on these observations, apply Extract Interface, too.

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\ImagineInterface;
41
use InvalidArgumentException;
42
use Psr\Http\Message\UploadedFileInterface;
43
use Throwable;
44
45
/**
46
 * A card containing an image and some information about it.
47
 */
48
#[ORM\Index(name: 'card_name_idx', columns: ['name'])]
49
#[ORM\Index(name: 'card_plain_name_idx', columns: ['plain_name'])]
50
#[ORM\Index(name: 'card_locality_idx', columns: ['locality'])]
51
#[ORM\Index(name: 'card_area_idx', columns: ['area'])]
52
#[ORM\Index(
53
    name: 'FULLTEXT__CARD_CUSTOM_SEARCH',
54
    flags: ['fulltext'],
55
    fields: [
56
        'dating',
57
        'cachedArtistNames',
58
        'addition',
59
        'expandedName',
60
        'material',
61
        'techniqueAuthor',
62
        'objectReference',
63
        'corpus',
64
        'street',
65
        'locality',
66
        'code',
67
        'name',
68
    ],
69
)]
70
#[ORM\Index(name: 'FULLTEXT__CARD_LOCALITY', flags: ['fulltext'], fields: ['locality'])]
71
#[ORM\Index(name: 'FULLTEXT__CARD_NAMES', flags: ['fulltext'], fields: ['name', 'expandedName'])]
72
#[ORM\UniqueConstraint(name: 'unique_code', columns: ['code', 'site'])]
73
#[API\Filter(field: 'nameOrExpandedName', operator: NameOrExpandedNameOperatorType::class, type: 'string')]
74
#[API\Filter(field: 'localityOrInstitutionLocality', operator: LocalityOrInstitutionLocalityOperatorType::class, type: 'string')]
75
#[API\Filter(field: 'datingYearRange', operator: DatingYearRangeOperatorType::class, type: 'int')]
76
#[API\Filter(field: 'cardYearRange', operator: CardYearRangeOperatorType::class, type: 'int')]
77
#[API\Filter(field: 'custom', operator: LocationOperatorType::class, type: 'string')]
78
#[API\Sorting(Artists::class)]
79
#[API\Sorting(Domains::class)]
80
#[API\Sorting(InstitutionLocality::class)]
81
#[API\Sorting(\Application\Api\Input\Sorting\DocumentType::class)]
82
#[ORM\HasLifecycleCallbacks]
83
#[ORM\Entity(CardRepository::class)]
84
class Card extends AbstractModel implements HasSiteInterface, Image
85
{
86
    use CardSimpleProperties;
87
    use HasAddress;
88
    use HasCode;
89
    use HasFileSize;
90
    use HasImage {
91
        setFile as traitSetFile;
92
    }
93
    use HasInstitution;
94
    use HasRichTextName;
95
    use HasSite;
96
    use HasYearRange;
97
98
    private const IMAGE_PATH = 'data/images/';
99
100
    #[ORM\Column(type: 'CardVisibility', options: ['default' => CardVisibility::Private])]
101
    private CardVisibility $visibility = CardVisibility::Private;
102
103
    #[ORM\Column(type: 'integer')]
104
    private int $width = 0;
105
106
    #[ORM\Column(type: 'integer')]
107
    private int $height = 0;
108
109
    #[ORM\Column(type: 'string', options: ['default' => ''])]
110
    private string $dating = '';
111
112
    /**
113
     * This is a form of cache of all artist names whose only purpose is to be able
114
     * to search on artists more easily. It is automatically maintained via DB triggers.
115
     */
116
    #[API\Exclude]
117
    #[ORM\Column(type: 'text', options: ['default' => ''])]
118
    private string $cachedArtistNames = '';
119
120
    /**
121
     * @var DoctrineCollection<Collection>
122
     */
123
    #[ORM\ManyToMany(targetEntity: Collection::class)]
124
    private DoctrineCollection $collections;
125
126
    /**
127
     * @var DoctrineCollection<Artist>
128
     */
129
    #[ORM\ManyToMany(targetEntity: Artist::class)]
130
    private DoctrineCollection $artists;
131
132
    /**
133
     * @var DoctrineCollection<AntiqueName>
134
     */
135
    #[ORM\ManyToMany(targetEntity: AntiqueName::class)]
136
    private DoctrineCollection $antiqueNames;
137
138
    /**
139
     * @var DoctrineCollection<Tag>
140
     */
141
    #[ORM\ManyToMany(targetEntity: Tag::class)]
142
    private DoctrineCollection $tags;
143
144
    /**
145
     * @var DoctrineCollection<Dating>
146
     */
147
    #[ORM\OneToMany(targetEntity: Dating::class, mappedBy: 'card')]
148
    private DoctrineCollection $datings;
149
150
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
151
    #[ORM\ManyToOne(targetEntity: self::class)]
152
    private ?Card $original = null;
153
154
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
155
    #[ORM\ManyToOne(targetEntity: DocumentType::class)]
156
    private ?DocumentType $documentType = null;
157
158
    /**
159
     * @var DoctrineCollection<Domain>
160
     */
161
    #[ORM\ManyToMany(targetEntity: Domain::class)]
162
    private DoctrineCollection $domains;
163
164
    /**
165
     * @var DoctrineCollection<Period>
166
     */
167
    #[ORM\ManyToMany(targetEntity: Period::class)]
168
    private DoctrineCollection $periods;
169
170
    /**
171
     * @var DoctrineCollection<Material>
172
     */
173
    #[ORM\ManyToMany(targetEntity: Material::class)]
174
    private DoctrineCollection $materials;
175
176
    /**
177
     * @var DoctrineCollection<Card>
178
     */
179
    #[ORM\ManyToMany(targetEntity: self::class)]
180
    private DoctrineCollection $cards;
181
182
    /**
183
     * There is actually 0 to 1 change, never more. And this is
184
     * enforced by DB unique constraints on the mapping side.
185
     *
186
     * @var DoctrineCollection<Change>
187
     */
188
    #[ORM\OneToMany(targetEntity: Change::class, mappedBy: 'suggestion')]
189
    private DoctrineCollection $changes;
190
191
    #[ORM\Column(type: 'string', length: 191, options: ['default' => ''])]
192
    private string $documentSize = '';
193
194
    #[ORM\Column(name: 'legacy_id', type: 'integer', nullable: true)]
195
    private ?int $legacyId = null;
196
197
    /**
198
     * Constructor.
199
     */
200 48
    public function __construct(string $name = '')
201
    {
202 48
        $this->setName($name);
203
204 48
        $this->changes = new ArrayCollection();
205 48
        $this->collections = new ArrayCollection();
206 48
        $this->artists = new ArrayCollection();
207 48
        $this->antiqueNames = new ArrayCollection();
208 48
        $this->tags = new ArrayCollection();
209 48
        $this->datings = new ArrayCollection();
210 48
        $this->cards = new ArrayCollection();
211 48
        $this->domains = new ArrayCollection();
212 48
        $this->periods = new ArrayCollection();
213 48
        $this->materials = new ArrayCollection();
214
    }
215
216
    /**
217
     * Return whether this is publicly available to everybody, or only member, or only owner.
218
     */
219 17
    public function getVisibility(): CardVisibility
220
    {
221 17
        return $this->visibility;
222
    }
223
224
    /**
225
     * Set whether this is publicly available to everybody, or only member, or only owner.
226
     */
227 27
    public function setVisibility(CardVisibility $visibility): void
228
    {
229 27
        if ($this->visibility === $visibility) {
230 11
            return;
231
        }
232
233 24
        $user = User::getCurrent();
234 24
        if ($visibility === CardVisibility::Public && $user->getRole() !== User::ROLE_ADMINISTRATOR) {
235 2
            throw new Exception('Only administrator can make a card public');
236
        }
237
238 23
        $this->visibility = $visibility;
239
    }
240
241
    /**
242
     * Get collections this card belongs to.
243
     */
244 10
    public function getCollections(): DoctrineCollection
245
    {
246 10
        return $this->collections;
247
    }
248
249
    /**
250
     * Get the card dating.
251
     *
252
     * This is a free form string that will be parsed to **try** and extract
253
     * some actual date range of dates. Any string is valid, but some parseable
254
     * values would typically be:
255
     *
256
     * - (1620-1652)
257
     * - 01.05.1917
258
     * - XIIIe siècle
259
     * - 1927
260
     * - c. 1100
261
     * - Fin du XIIe siècle
262
     */
263 5
    public function getDating(): string
264
    {
265 5
        return $this->dating;
266
    }
267
268
    /**
269
     * Set the card dating.
270
     *
271
     * This is a free form string that will be parsed to **try** and extract
272
     * some actual date range of dates. Any string is valid, but some parseable
273
     * values would typically be:
274
     *
275
     * - (1620-1652)
276
     * - 01.05.1917
277
     * - XIIIe siècle
278
     * - 1927
279
     * - c. 1100
280
     * - Fin du XIIe siècle
281
     */
282 10
    public function setDating(string $dating): void
283
    {
284 10
        if ($dating === $this->dating) {
285 1
            return;
286
        }
287 10
        $this->dating = $dating;
288
289 10
        $this->computeDatings();
290
    }
291
292
    /**
293
     * Return the automatically computed dating periods.
294
     */
295 4
    public function getDatings(): DoctrineCollection
296
    {
297 4
        return $this->datings;
298
    }
299
300
    /**
301
     * Set all artists at once by their names.
302
     *
303
     * Non-existing artists will be created automatically.
304
     *
305
     * @param null|string[] $artistNames
306
     */
307 9
    public function setArtists(?array $artistNames): void
308
    {
309 9
        if (null === $artistNames) {
0 ignored issues
show
introduced by
The condition null === $artistNames is always false.
Loading history...
310
            return;
311
        }
312
313 9
        $artistRepository = _em()->getRepository(Artist::class);
314 9
        $newArtists = $artistRepository->getOrCreateByNames($artistNames, $this->getSite());
315
316 9
        $oldIds = Utility::modelToId($this->artists->toArray());
317 9
        sort($oldIds);
318
319 9
        $newIds = Utility::modelToId($newArtists);
320 9
        sort($newIds);
321
322 9
        if ($oldIds === $newIds && !in_array(null, $oldIds, true) && !in_array(null, $newIds, true)) {
323
            return;
324
        }
325
326 9
        $this->artists->clear();
327 9
        foreach ($newArtists as $a) {
328 9
            $this->artists->add($a);
329
        }
330
    }
331
332
    /**
333
     * Set all materials at once.
334
     *
335
     * @param null|string[] $materials
336
     */
337 7
    #[API\Input(type: '?ID[]')]
338
    public function setMaterials(?array $materials): void
339
    {
340 7
        if (null === $materials) {
0 ignored issues
show
introduced by
The condition null === $materials is always false.
Loading history...
341
            return;
342
        }
343
344 7
        $this->setEntireCollection($materials, $this->materials, Material::class);
345 7
        $this->addEntireHierarchy($this->materials);
346
    }
347
348
    /**
349
     * Set all antiqueNames at once.
350
     *
351
     * @param null|string[] $antiqueNames
352
     */
353 8
    #[API\Input(type: '?ID[]')]
354
    public function setAntiqueNames(?array $antiqueNames): void
355
    {
356 8
        if (null === $antiqueNames) {
0 ignored issues
show
introduced by
The condition null === $antiqueNames is always false.
Loading history...
357
            return;
358
        }
359
360 8
        $this->setEntireCollection($antiqueNames, $this->antiqueNames, AntiqueName::class);
361
    }
362
363
    /**
364
     * Set all domains at once.
365
     *
366
     * @param null|string[] $domains
367
     */
368
    #[API\Input(type: '?ID[]')]
369
    public function setDomains(?array $domains): void
370
    {
371
        if (null === $domains) {
0 ignored issues
show
introduced by
The condition null === $domains is always false.
Loading history...
372
            return;
373
        }
374
375
        $this->setEntireCollection($domains, $this->domains, Domain::class);
376
    }
377
378
    /**
379
     * Set all periods at once.
380
     *
381
     * @param null|string[] $periods
382
     */
383 7
    #[API\Input(type: '?ID[]')]
384
    public function setPeriods(?array $periods): void
385
    {
386 7
        if (null === $periods) {
0 ignored issues
show
introduced by
The condition null === $periods is always false.
Loading history...
387
            return;
388
        }
389
390 7
        $this->setEntireCollection($periods, $this->periods, Period::class);
391
    }
392
393
    /**
394
     * Set all tags at once.
395
     *
396
     * @param null|string[] $tags
397
     */
398 8
    #[API\Input(type: '?ID[]')]
399
    public function setTags(?array $tags): void
400
    {
401 8
        if (null === $tags) {
0 ignored issues
show
introduced by
The condition null === $tags is always false.
Loading history...
402
            return;
403
        }
404
405 8
        $this->setEntireCollection($tags, $this->tags, Tag::class);
406 8
        $this->addEntireHierarchy($this->tags);
407
    }
408
409 9
    private function setEntireCollection(array $ids, DoctrineCollection $collection, string $class): void
410
    {
411 9
        $oldIds = Utility::modelToId($collection->toArray());
412 9
        sort($oldIds);
413
414 9
        sort($ids);
415
416 9
        if ($oldIds === $ids && !in_array(null, $oldIds, true) && !in_array(null, $ids, true)) {
417
            return;
418
        }
419
420 9
        $repository = _em()->getRepository($class);
421 9
        $objects = $repository->findBy([
422 9
            'id' => $ids,
423 9
            'site' => $this->getSite(),
424 9
        ]);
425
426 9
        $collection->clear();
427 9
        foreach ($objects as $object) {
428 1
            $collection->add($object);
429
        }
430
    }
431
432
    /**
433
     * Get artists.
434
     */
435 11
    public function getArtists(): DoctrineCollection
436
    {
437 11
        return $this->artists;
438
    }
439
440
    /**
441
     * Get antiqueNames.
442
     */
443 1
    public function getAntiqueNames(): DoctrineCollection
444
    {
445 1
        return $this->antiqueNames;
446
    }
447
448
    /**
449
     * Add tag.
450
     */
451 1
    public function addTag(Tag $tag): void
452
    {
453 1
        if (!$this->tags->contains($tag)) {
454 1
            $this->tags[] = $tag;
455
        }
456 1
        $this->addEntireHierarchy($this->tags);
457
    }
458
459
    /**
460
     * Remove tag.
461
     */
462 1
    public function removeTag(Tag $tag): void
463
    {
464 1
        $this->tags->removeElement($tag);
465 1
        $this->addEntireHierarchy($this->tags);
466
    }
467
468
    /**
469
     * Get tags.
470
     */
471 1
    public function getTags(): DoctrineCollection
472
    {
473 1
        return $this->tags;
474
    }
475
476
    /**
477
     * The original card if this is a suggestion.
478
     */
479 4
    public function getOriginal(): ?self
480
    {
481 4
        return $this->original;
482
    }
483
484
    /**
485
     * Defines this card as suggestion for the $original.
486
     */
487 1
    public function setOriginal(?self $original): void
488
    {
489 1
        $this->original = $original;
490
    }
491
492 2
    public function getDocumentType(): ?DocumentType
493
    {
494 2
        return $this->documentType;
495
    }
496
497 4
    public function setDocumentType(?DocumentType $documentType): void
498
    {
499 4
        $this->documentType = $documentType;
500
    }
501
502
    /**
503
     * Get domains.
504
     */
505 5
    public function getDomains(): DoctrineCollection
506
    {
507 5
        return $this->domains;
508
    }
509
510
    /**
511
     * Add Domain.
512
     */
513 1
    public function addDomain(Domain $domain): void
514
    {
515 1
        if (!$this->domains->contains($domain)) {
516 1
            $this->domains[] = $domain;
517
        }
518
    }
519
520
    /**
521
     * Get periods.
522
     */
523 3
    public function getPeriods(): DoctrineCollection
524
    {
525 3
        return $this->periods;
526
    }
527
528
    /**
529
     * Add Period.
530
     */
531 4
    public function addPeriod(Period $period): void
532
    {
533 4
        if (!$this->periods->contains($period)) {
534 4
            $this->periods[] = $period;
535
        }
536
    }
537
538
    /**
539
     * Remove Period.
540
     */
541
    public function removePeriod(Period $period): void
542
    {
543
        $this->periods->removeElement($period);
544
    }
545
546
    /**
547
     * Get materials.
548
     */
549 1
    public function getMaterials(): DoctrineCollection
550
    {
551 1
        return $this->materials;
552
    }
553
554
    /**
555
     * Add Material.
556
     */
557 4
    public function addMaterial(Material $material): void
558
    {
559 4
        if (!$this->materials->contains($material)) {
560 4
            $this->materials[] = $material;
561
        }
562
563 4
        $this->addEntireHierarchy($this->materials);
564
    }
565
566
    /**
567
     * Remove Material.
568
     */
569
    public function removeMaterial(Material $material): void
570
    {
571
        $this->materials->removeElement($material);
572
        $this->addEntireHierarchy($this->materials);
573
    }
574
575
    /**
576
     * Add this card into the given collection.
577
     */
578 6
    public function addCollection(Collection $collection): void
579
    {
580 6
        if (!$this->collections->contains($collection)) {
581 6
            $this->collections->add($collection);
582
        }
583
584
        // If we are new and don't have a code yet, set one automatically
585 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...
586
            /** @var CardRepository $userRepository */
587 1
            $userRepository = _em()->getRepository(self::class);
588 1
            $code = $userRepository->getNextCodeAvailable($collection);
589 1
            $this->setCode($code);
590
        }
591
    }
592
593
    /**
594
     * Remove this card from given collection.
595
     */
596 2
    public function removeCollection(Collection $collection): void
597
    {
598 2
        $this->collections->removeElement($collection);
599
    }
600
601
    /**
602
     * Notify the Card that a Dating was added.
603
     * This should only be called by Dating::setCard().
604
     */
605 4
    public function datingAdded(Dating $dating): void
606
    {
607 4
        $this->datings->add($dating);
608
    }
609
610
    /**
611
     * Notify the Card that a Dating was removed.
612
     * This should only be called by Dating::setCard().
613
     */
614 1
    public function datingRemoved(Dating $dating): void
615
    {
616 1
        $this->datings->removeElement($dating);
617
    }
618
619
    /**
620
     * Get image width.
621
     */
622 7
    public function getWidth(): int
623
    {
624 7
        return $this->width;
625
    }
626
627
    /**
628
     * Set image width.
629
     */
630 11
    #[API\Exclude]
631
    public function setWidth(int $width): void
632
    {
633 11
        $this->width = $width;
634
    }
635
636
    /**
637
     * Get image height.
638
     */
639 12
    public function getHeight(): int
640
    {
641 12
        return $this->height;
642
    }
643
644
    /**
645
     * Set image height.
646
     */
647 11
    #[API\Exclude]
648
    public function setHeight(int $height): void
649
    {
650 11
        $this->height = $height;
651
    }
652
653
    /**
654
     * Set the image file.
655
     */
656 9
    #[API\Input(type: '?GraphQL\Upload\UploadType')]
657
    public function setFile(UploadedFileInterface $file): void
658
    {
659
        global $container;
660
661 9
        $this->traitSetFile($file);
662
663
        try {
664 9
            $this->readFileInfo();
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
     * Read dimension and size from file on disk.
695
     */
696 9
    private function readFileInfo(): void
697
    {
698
        global $container;
699 9
        $path = $this->getPath();
700
701
        /** @var ImagineInterface $imagine */
702 9
        $imagine = $container->get(ImagineInterface::class);
703 9
        $image = $imagine->open($path);
704
705
        // Auto-rotate image if EXIF says it's rotated
706 9
        $autorotate = new Autorotate();
707 9
        $autorotate->apply($image);
708 9
        $image->save($path);
709
710 9
        $size = $image->getSize();
711
712
        // Ensure that we read fresh stats from disk.
713 9
        clearstatcache(true, $path);
714
715 9
        $this->setWidth($size->getWidth());
716 9
        $this->setHeight($size->getHeight());
717 9
        $this->setFileSize(filesize($path));
718
    }
719
720 12
    private function computeDatings(): void
721
    {
722 12
        $rule = new DatingRule();
723
724
        // Delete all existing
725 12
        foreach ($this->datings as $d) {
726 2
            _em()->remove($d);
727
        }
728 12
        $this->datings->clear();
729
730
        // Add new one
731 12
        $datings = $rule->compute($this->dating);
732 12
        foreach ($datings as $d) {
733 3
            _em()->persist($d);
734 3
            $d->setCard($this);
735
        }
736
    }
737
738
    /**
739
     * Copy most of this card data into the given card.
740
     */
741 3
    public function copyInto(self $original): void
742
    {
743
        // Trigger loading of proxy
744 3
        $original->getName();
745
746 3
        $blacklist = [
747 3
            'id',
748 3
            'visibility',
749 3
            'code',
750 3
            '__initializer__',
751 3
            '__cloner__',
752 3
            '__isInitialized__',
753 3
        ];
754
755 3
        if (!$this->hasImage()) {
756 1
            $blacklist[] = 'filename';
757 1
            $blacklist[] = 'width';
758 1
            $blacklist[] = 'height';
759 1
            $blacklist[] = 'fileSize';
760
        }
761
762
        // Copy scalars
763 3
        foreach ($this as $property => $value) {
764 3
            if (in_array($property, $blacklist, true)) {
765 3
                continue;
766
            }
767
768 3
            if (is_scalar($value) || $value === null) {
769 3
                $original->$property = $value;
770
            }
771
        }
772
773
        // Copy a few collection and entities
774 3
        $original->artists = clone $this->artists;
775 3
        $original->tags = clone $this->tags;
776 3
        $original->materials = clone $this->materials;
777 3
        $original->domains = clone $this->domains;
778 3
        $original->periods = clone $this->periods;
779 3
        $original->computeDatings();
780 3
        $original->institution = $this->institution;
781 3
        $original->country = $this->country;
782 3
        $original->documentType = $this->documentType;
783
784
        // Copy file on disk
785 3
        if ($this->filename) {
786 2
            $original->generateUniqueFilename($this->filename);
787 2
            copy($this->getPath(), $original->getPath());
788
        }
789
    }
790
791
    /**
792
     * Get related cards.
793
     */
794 2
    public function getCards(): DoctrineCollection
795
    {
796 2
        return $this->cards;
797
    }
798
799
    /**
800
     * Add related card.
801
     */
802 3
    public function addCard(self $card): void
803
    {
804 3
        if ($card === $this) {
805 1
            throw new InvalidArgumentException('A card cannot be related to itself');
806
        }
807
808 2
        if (!$this->cards->contains($card)) {
809 2
            $this->cards[] = $card;
810
        }
811
812 2
        if (!$card->getCards()->contains($this)) {
813 2
            $card->getCards()->add($this);
814
        }
815
    }
816
817
    /**
818
     * Remove related card.
819
     */
820 1
    public function removeCard(self $card): void
821
    {
822 1
        $this->cards->removeElement($card);
823 1
        $card->getCards()->removeElement($this);
824
    }
825
826
    /**
827
     * Return the change this card is a suggestion for, if any.
828
     */
829 4
    public function getChange(): ?Change
830
    {
831 4
        return $this->changes->first() ?: null;
832
    }
833
834
    /**
835
     * Notify the Card that it was added to a Change.
836
     * This should only be called by Change::addCard().
837
     */
838 4
    public function changeAdded(?Change $change): void
839
    {
840 4
        $this->changes->clear();
841 4
        if ($change) {
842 4
            $this->changes->add($change);
843
        }
844
    }
845
846
    /**
847
     * Set documentSize.
848
     */
849 7
    public function setDocumentSize(string $documentSize): void
850
    {
851 7
        $this->documentSize = $documentSize;
852
    }
853
854
    /**
855
     * Get documentSize.
856
     */
857
    public function getDocumentSize(): string
858
    {
859
        return $this->documentSize;
860
    }
861
862 8
    public function setIsbn(string $isbn): void
863
    {
864
        // Field is readonly and can only be emptied (Dilps only).
865 8
        if ($this->getSite() === Site::Dilps && $isbn !== '') {
866 8
            return;
867
        }
868
869
        $this->isbn = $isbn;
870
    }
871
872
    /**
873
     * Ensure that the entire hierarchy is added, but also make sure that
874
     * a non-leaf tag is added without one of his leaf.
875
     */
876 12
    private function addEntireHierarchy(DoctrineCollection $collection): void
877
    {
878 12
        $objects = $collection->toArray();
879 12
        $collection->clear();
880
881
        /** @var HasParentInterface $object */
882 12
        foreach ($objects as $object) {
883 5
            if ($object->hasChildren()) {
884 1
                continue;
885
            }
886
887 5
            $collection->add($object);
888
889 5
            foreach ($object->getParentHierarchy() as $parent) {
890 2
                if (!$collection->contains($parent)) {
891 2
                    $collection->add($parent);
892
                }
893
            }
894
        }
895
    }
896
}
897