Passed
Push — master ( bf8b93...f11b74 )
by Chauncey
03:06
created

Attachment::fileAndLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Charcoal\Attachment\Object;
4
5
use Charcoal\Model\ModelInterface;
6
use Exception;
7
use ReflectionClass;
8
use RuntimeException;
9
use InvalidArgumentException;
10
11
// From PSR-7
12
use Psr\Http\Message\UriInterface;
13
14
// From Pimple
15
use Pimple\Container;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Charcoal\Attachment\Object\Container.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
17
// From 'charcoal-core'
18
use Charcoal\Loader\CollectionLoader;
19
20
// From 'charcoal-object'
21
use Charcoal\Object\Content;
22
23
// From 'charcoal-translator'
24
use Charcoal\Translator\Translation;
25
26
// From 'charcoal-attachment'
27
use Charcoal\Attachment\Interfaces\AttachableInterface;
28
use Charcoal\Attachment\Interfaces\AttachmentContainerInterface;
29
use Charcoal\Attachment\Object\File;
30
use Charcoal\Attachment\Object\Image;
31
use Charcoal\Attachment\Object\Text;
32
use Charcoal\Attachment\Object\Embed;
33
use Charcoal\Attachment\Object\Video;
34
use Charcoal\Attachment\Object\Gallery;
35
use Charcoal\Attachment\Object\Accordion;
36
use Charcoal\Attachment\Object\Link;
37
use Charcoal\Attachment\Object\Container as AttachmentContainer;
38
use Charcoal\Attachment\Object\Join;
39
40
/**
41
 *
42
 */
43
class Attachment extends Content implements AttachableInterface
0 ignored issues
show
Bug introduced by
There is at least one abstract method in this class. Maybe declare it as abstract, or implement the remaining methods: hasProperty, p, properties, property
Loading history...
44
{
45
    /**
46
     * Default attachment types
47
     */
48
    const FILE_TYPE      = File::class;
49
    const LINK_TYPE      = Link::class;
50
    const IMAGE_TYPE     = Image::class;
51
    const EMBED_TYPE     = Embed::class;
52
    const VIDEO_TYPE     = Video::class;
53
    const TEXT_TYPE      = Text::class;
54
    const GALLERY_TYPE   = Gallery::class;
55
    const ACCORDION_TYPE = Accordion::class;
56
    const CONTAINER_TYPE = AttachmentContainer::class;
57
58
    /**
59
     * The attachment type.
60
     *
61
     * @var string
62
     */
63
    protected $type;
64
65
    /**
66
     * The attachment heading template.
67
     *
68
     * @var Translation|string|null
69
     */
70
    protected $heading;
71
72
    /**
73
     * The attachment preview template.
74
     *
75
     * @var Translation|string|null
76
     */
77
    protected $preview;
78
79
    /**
80
     * Whether to show the title on the front-end.
81
     *
82
     * @var boolean
83
     */
84
    protected $showTitle = true;
85
86
    /**
87
     * @var string|string[]
88
     */
89
    protected $categories;
90
91
    /**
92
     * Generic information about the attachment.
93
     *
94
     * @var Translation|string|null $title       The title of the attachment.
95
     * @var Translation|string|null $subtitle    The subtitle of the attachment.
96
     * @var Translation|string|null $description The content of the attachment.
97
     * @var Translation|string|null $keywords    Keywords finding the attachment.
98
     */
99
    protected $title;
100
    protected $subtitle;
101
    protected $description;
102
    protected $keywords;
103
104
    /**
105
     * File related attachments.
106
     *
107
     * @var string  $file      The path of an attached file.
108
     * @var string  $fileLabel The label for the attached file.
109
     * @var integer $fileSize  The size of the attached file in bytes.
110
     * @var string  $fileType  The content type of the attached file.
111
     */
112
    protected $file;
113
    protected $fileLabel;
114
    protected $fileSize;
115
    protected $fileType;
116
117
    /**
118
     * Link related attachments.
119
     *
120
     * @var string $link      The URL related to the attachment.
121
     * @var string $linkLabel The label for the attached link.
122
     */
123
    protected $link;
124
    protected $linkLabel;
125
126
    /**
127
     * Path to a thumbnail of the attached file.
128
     *
129
     * Auto-generated thumbnail if the attached file is an image.
130
     *
131
     * @var Translation|string|null
132
     */
133
    protected $thumbnail;
134
135
    /**
136
     * Embedded content.
137
     *
138
     * @var Translation|string|null
139
     */
140
    protected $embed;
141
142
    /**
143
     * The attachment's position amongst other attachments.
144
     *
145
     * @var integer
146
     */
147
    protected $position;
148
149
    /**
150
     * The base URI.
151
     *
152
     * @var UriInterface|null
153
     */
154
    private $baseUrl;
155
156
    /**
157
     * Whether the attachment acts like a presenter (TRUE) or data model (FALSE).
158
     *
159
     * @var boolean
160
     */
161
    private $presentable = false;
162
163
    /**
164
     * The attachment's parent container instance.
165
     *
166
     * @var AttachmentContainerInterface|null
167
     */
168
    protected $containerObj;
169
170
    /**
171
     * A store of resolved attachment types.
172
     *
173
     * @var array
174
     */
175
    protected static $resolvedType = [];
176
177
    /**
178
     * Store the collection loader for the current class.
179
     *
180
     * @var CollectionLoader
181
     */
182
    private $collectionLoader;
183
184
    /**
185
     * @var ModelInterface $presenter
186
     */
187
    private $presenter;
188
189
    /**
190
     * Return a new section object.
191
     *
192
     * @param array $data Dependencies.
193
     */
194
    public function __construct(array $data = null)
195
    {
196
        parent::__construct($data);
197
198
        if (is_callable([ $this, 'defaultData' ])) {
199
            $defaultData = $this->metadata()->defaultData();
200
            if ($defaultData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $defaultData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
201
                $this->setData($defaultData);
202
            }
203
        }
204
    }
205
206
    /**
207
     * Inject dependencies from a DI Container.
208
     *
209
     * @param  Container $container A dependencies container instance.
210
     * @return void
211
     */
212
    protected function setDependencies(Container $container)
213
    {
214
        parent::setDependencies($container);
215
216
        $this->setBaseUrl($container['base-url']);
217
        $this->setCollectionLoader($container['model/collection/loader']);
218
    }
219
220
    /**
221
     * Determine if the model is for presentation or editing.
222
     *
223
     * @param  boolean $presenter The presenter flag.
224
     * @return boolean Returns TRUE if model is used for presentation; FALSE for editing.
225
     */
226
    public function isPresentable($presenter = null)
227
    {
228
        if (is_bool($presenter)) {
229
            $this->presentable = $presenter;
230
        }
231
232
        return $this->presentable;
233
    }
234
235
    /**
236
     * Retrieve the attachment's container ID (if any).
237
     *
238
     * Useful when templating a container of nested attachments.
239
     *
240
     * @return mixed|null
241
     */
242
    public function containerId()
243
    {
244
        $container = $this->containerObj();
245
        if ($container) {
246
            return $container->id();
247
        }
248
249
        return null;
250
    }
251
252
    /**
253
     * Determine if the attachment belongs to a container.
254
     *
255
     * @return boolean
256
     */
257
    public function hasContainerObj()
258
    {
259
        return boolval($this->containerObj);
260
    }
261
262
    /**
263
     * Retrieve the attachment's container instance.
264
     *
265
     * @return AttachmentContainerInterface|null
266
     */
267
    public function containerObj()
268
    {
269
        return $this->containerObj;
270
    }
271
272
    /**
273
     * Set the attachment's container instance.
274
     *
275
     * @param  AttachmentContainerInterface|null $obj The container object or NULL.
276
     * @throws InvalidArgumentException If the given object is invalid.
277
     * @return Attachment
278
     */
279
    public function setContainerObj($obj)
280
    {
281
        if ($obj === null) {
282
            $this->containerObj = null;
283
284
            return $this;
285
        }
286
287 View Code Duplication
        if (!$obj instanceof AttachmentContainerInterface) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
288
            throw new InvalidArgumentException(sprintf(
289
                'Container object must be an instance of %s; received %s',
290
                AttachmentContainerInterface::class,
291
                (is_object($obj) ? get_class($obj) : gettype($obj))
292
            ));
293
        }
294
295 View Code Duplication
        if (!$obj->id()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
296
            throw new InvalidArgumentException(sprintf(
297
                'Container object must have an ID.',
298
                (is_object($obj) ? get_class($obj) : gettype($obj))
299
            ));
300
        }
301
302
        $this->containerObj = $obj;
303
304
        return $this;
305
    }
306
307
    /**
308
     * Retrieve the attachment type.
309
     *
310
     * @return string
311
     */
312
    public function type()
313
    {
314
        if (!$this->type) {
315
            $this->type = $this->objType();
316
        }
317
318
        return $this->type;
319
    }
320
321
    /**
322
     * Set the attachment type.
323
     *
324
     * @param  string $type The attachment type.
325
     * @throws InvalidArgumentException If provided argument is not of type 'string'.
326
     * @return string
327
     */
328
    public function setType($type)
329
    {
330
        if (!is_string($type)) {
331
            throw new InvalidArgumentException('Attachment type must be a string.');
332
        }
333
334
        $this->type = $type;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Retrieve the label of the attachment type.
341
     *
342
     * @return string Returns the translated attachment type or the short name.
343
     */
344
    public function typeLabel()
345
    {
346
        $type  = $this->type();
347
        $label = $this->translator()->translate($type);
348
349
        if ($type === $label) {
350
            $label = ucfirst($this->microType());
351
        }
352
353
        return $label;
354
    }
355
356
    /**
357
     * Retrieve the unqualified class name.
358
     *
359
     * @return string Returns the short name of the model's class, the part without the namespace.
360
     */
361
    public function microType()
362
    {
363
        $classname = get_called_class();
364
365
        if (!isset(static::$resolvedType[$classname])) {
366
            $reflect = new ReflectionClass($this);
367
368
            static::$resolvedType[$classname] = strtolower($reflect->getShortName());
369
        }
370
371
        return static::$resolvedType[$classname];
372
    }
373
374
    /**
375
     * Retrieve the image attachment type.
376
     *
377
     * @return string
378
     */
379
    public function imageType()
380
    {
381
        return self::IMAGE_TYPE;
382
    }
383
384
    /**
385
     * Retrieve the attachment's heading template.
386
     *
387
     * @return Translation|string|null
388
     */
389
    public function heading()
390
    {
391
        $heading = $this->render((string)$this->heading);
392
393
        if (!$heading) {
394
            $heading = $this->translator()->translation('{{ objType }} #{{ id }}', [
395
                '{{ objType }}' => $this->typeLabel(),
396
                '{{ id }}'      => $this->id()
397
            ]);
398
        }
399
400
        return $heading;
401
    }
402
403
    /**
404
     * Retrieve the attachment's heading as a raw value.
405
     *
406
     * @return Translation|string|null
407
     */
408
    public function rawHeading()
409
    {
410
        return $this->heading;
411
    }
412
413
    /**
414
     * Set the attachment's heading template.
415
     *
416
     * @param  string $template The attachment heading.
417
     * @return Attachment Chainable
418
     */
419
    public function setHeading($template)
420
    {
421
        $this->heading = $this->translator()->translation($template);
422
423
        return $this;
424
    }
425
426
    /**
427
     * Retrieve the attachment's preview template.
428
     *
429
     * @return Translation|string|null
430
     */
431
    public function preview()
432
    {
433
        if ($this->preview) {
434
            return $this->render((string)$this->preview);
435
        }
436
437
        return '';
438
    }
439
440
    /**
441
     * Retrieve the attachment's preview as a raw value.
442
     *
443
     * @return Translation|string|null
444
     */
445
    public function rawPreview()
446
    {
447
        return $this->preview;
448
    }
449
450
    /**
451
     * Set the attachment's preview template.
452
     *
453
     * @param  string $template The attachment preview.
454
     * @return Attachment Chainable
455
     */
456
    public function setPreview($template)
457
    {
458
        $this->preview = $this->translator()->translation($template);
459
460
        return $this;
461
    }
462
463
    /**
464
     * Determine if the attachment type is an image.
465
     *
466
     * @return boolean
467
     */
468
    public function isImage()
469
    {
470
        return ($this->microType() === 'image');
471
    }
472
473
    /**
474
     * Determine if the attachment type is an embed object.
475
     *
476
     * @return boolean
477
     */
478
    public function isEmbed()
479
    {
480
        return ($this->microType() === 'embed');
481
    }
482
483
    /**
484
     * Determine if the attachment type is a video.
485
     *
486
     * @return boolean
487
     */
488
    public function isVideo()
489
    {
490
        return ($this->microType() === 'video');
491
    }
492
493
    /**
494
     * Determine if the attachment type is a file attachment.
495
     *
496
     * @return boolean
497
     */
498
    public function isFile()
499
    {
500
        return ($this->microType() === 'file');
501
    }
502
503
    /**
504
     * Determine if the attachment type is a text-area.
505
     *
506
     * @return boolean
507
     */
508
    public function isText()
509
    {
510
        return ($this->microType() === 'text');
511
    }
512
513
    /**
514
     * Determine if the attachment type is an image gallery.
515
     *
516
     * @return boolean
517
     */
518
    public function isGallery()
519
    {
520
        return ($this->microType() === 'gallery');
521
    }
522
523
    /**
524
     * Determine if the attachment type is an accordion.
525
     *
526
     * @return boolean
527
     */
528
    public function isAccordion()
529
    {
530
        return ($this->microType() === 'accordion');
531
    }
532
533
    /**
534
     * Determine if the attachment type is a link.
535
     *
536
     * @return boolean
537
     */
538
    public function isLink()
539
    {
540
        return ($this->microType() === 'link');
541
    }
542
543
    /**
544
     * Determine if this attachment is a container.
545
     *
546
     * @return boolean
547
     */
548
    public function isAttachmentContainer()
549
    {
550
        return ($this instanceof AttachmentContainerInterface);
551
    }
552
553
    // Setters
554
    // =============================================================================
555
556
    /**
557
     * Show/hide the attachment's title on the front-end.
558
     *
559
     * @param  boolean $show Show (TRUE) or hide (FALSE) the title.
560
     * @return UiItemInterface Chainable
561
     */
562
    public function setShowTitle($show)
563
    {
564
        $this->showTitle = !!$show;
565
566
        return $this;
567
    }
568
569
    /**
570
     * Set the attachment's title.
571
     *
572
     * @param  string $title The object title.
573
     * @return self
574
     */
575
    public function setTitle($title)
576
    {
577
        $this->title = $this->translator()->translation($title);
578
579
        return $this;
580
    }
581
582
    /**
583
     * Set the attachment's sub-title.
584
     *
585
     * @param  string $title The object title.
586
     * @return self
587
     */
588
    public function setSubtitle($title)
589
    {
590
        $this->subtitle = $this->translator()->translation($title);
591
592
        return $this;
593
    }
594
595
    /**
596
     * Set the attachment's description.
597
     *
598
     * @param  string $description The description of the object.
599
     * @return self
600
     */
601
    public function setDescription($description)
602
    {
603
        $this->description = $this->translator()->translation($description);
604
605
        if ($this->isPresentable() && $this->description) {
606
            foreach ($this->description->data() as $lang => $trans) {
607
                $this->description[$lang] = $this->resolveUrls($trans);
608
            }
609
        }
610
611
        return $this;
612
    }
613
614
    /**
615
     * Set the attachment's keywords.
616
     *
617
     * @param  string|string[] $keywords One or more entries.
618
     * @return self
619
     */
620
    public function setKeywords($keywords)
621
    {
622
        $this->keywords = $keywords;
623
624
        return $this;
625
    }
626
627
    /**
628
     * Set the path to the thumbnail associated with the object.
629
     *
630
     * @param  string $path A path to an image.
631
     * @return self
632
     */
633
    public function setThumbnail($path)
634
    {
635
        $this->thumbnail = $this->translator()->translation($path);
636
637
        return $this;
638
    }
639
640
    /**
641
     * Set the path to the attached file.
642
     *
643
     * @param  string $path A path to a file.
644
     * @return self
645
     */
646
    public function setFile($path)
647
    {
648
        $this->file = $this->translator()->translation($path);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($path) can also be of type object<Charcoal\Translator\Translation>. However, the property $file is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
649
650
        return $this;
651
    }
652
653
    /**
654
     * Set the URL.
655
     *
656
     * @param  string $link An external url.
657
     * @return self
658
     */
659
    public function setLink($link)
660
    {
661
        $this->link = $this->translator()->translation($link);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($link) can also be of type object<Charcoal\Translator\Translation>. However, the property $link is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
662
663
        return $this;
664
    }
665
666
    /**
667
     * Set the file label.
668
     *
669
     * @param  string $label A descriptor.
670
     * @return self
671
     */
672
    public function setFileLabel($label)
673
    {
674
        $this->fileLabel = $this->translator()->translation($label);
675
676
        return $this;
677
    }
678
679
    /**
680
     * Set the link label.
681
     *
682
     * @param  string $label A descriptor.
683
     * @return self
684
     */
685
    public function setLinkLabel($label)
686
    {
687
        $this->linkLabel = $this->translator()->translation($label);
688
689
        return $this;
690
    }
691
692
    /**
693
     * Set the size of the attached file.
694
     *
695
     * @param  integer|float $size A file size in bytes; the one of the attached.
696
     * @throws InvalidArgumentException If provided argument is not of type 'integer' or 'float'.
697
     * @return self
698
     */
699 View Code Duplication
    public function setFileSize($size)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
700
    {
701
        if ($size === null) {
702
            $this->fileSize = null;
703
704
            return $this;
705
        }
706
707
        if (!is_numeric($size)) {
708
            throw new InvalidArgumentException('File size must be an integer or a float.');
709
        }
710
711
        $this->fileSize = $size;
712
713
        return $this;
714
    }
715
716
    /**
717
     * Set file extension.
718
     *
719
     * @param  string $type File extension.
720
     * @return self
721
     */
722
    public function setFileType($type)
723
    {
724
        $this->fileType = $type;
725
726
        return $this;
727
    }
728
729
    /**
730
     * Set the embed content.
731
     *
732
     * @param  string $embed A URI or an HTML media element.
733
     * @throws InvalidArgumentException If provided argument is not of type 'string'.
734
     * @return self
735
     */
736
    public function setEmbed($embed)
737
    {
738
        $this->embed = $this->translator()->translation($embed);
739
740
        return $this;
741
    }
742
743
    /**
744
     * @param string|\string[] $categories Category elements.
745
     * @return self
746
     */
747
    public function setCategories($categories)
748
    {
749
        $this->categories = $categories;
0 ignored issues
show
Documentation Bug introduced by
It seems like $categories can also be of type array<integer,object<string>>. However, the property $categories is declared as type string|array<integer,string>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
750
751
        return $this;
752
    }
753
754
    // Getters
755
    // =============================================================================
756
757
    /**
758
     * Determine if the title is to be displayed on the front-end.
759
     *
760
     * @return boolean
761
     */
762
    public function showTitle()
763
    {
764
        if (is_bool($this->showTitle)) {
765
            return $this->showTitle;
766
        } else {
767
            return !!$this->title();
768
        }
769
    }
770
771
    /**
772
     * Retrieve the attachment's title.
773
     *
774
     * @return Translation|string|null
775
     */
776
    public function title()
777
    {
778
        return $this->title;
779
    }
780
781
    /**
782
     * Retrieve the attachment's sub-title.
783
     *
784
     * @return Translation|string|null
785
     */
786
    public function subtitle()
787
    {
788
        return $this->subtitle;
789
    }
790
791
    /**
792
     * Retrieve attachment's description.
793
     *
794
     * @return Translation|string|null
795
     */
796
    public function description()
797
    {
798
        return $this->description;
799
    }
800
801
    /**
802
     * Retrieve the attachment's keywords.
803
     *
804
     * @return string[]
805
     */
806
    public function keywords()
807
    {
808
        return $this->keywords;
809
    }
810
811
    /**
812
     * Retrieve the path to the thumbnail associated with the object.
813
     *
814
     * @return string|null
815
     */
816
    public function thumbnail()
817
    {
818
        return $this->thumbnail;
819
    }
820
821
    /**
822
     * Retrieve the path to the attached file.
823
     *
824
     * @return Translation|string|null
825
     */
826
    public function file()
827
    {
828
        return $this->file;
829
    }
830
831
    /**
832
     * Retrieve the attached link.
833
     *
834
     * @return Translation|string|null
835
     */
836
    public function link()
837
    {
838
        return $this->link;
839
    }
840
841
    /**
842
     * Retrieve either the attached file or link.
843
     *
844
     * @return Translation|string|null
845
     */
846
    public function fileOrLink()
847
    {
848
        return $this['file'] ?: $this['link'];
849
    }
850
851
    /**
852
     * Retrieve the attached file(s) and link(s).
853
     *
854
     * @return string[]|null
855
     */
856
    public function fileAndLink()
857
    {
858
        $prop  = $this->property('file');
859
        $files = $prop->parseValAsFileList($this['file']);
0 ignored issues
show
Bug introduced by
The method parseValAsFileList() does not exist on Charcoal\Property\PropertyInterface. Did you maybe mean parseVal()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
860
        $links = $prop->parseValAsFileList($this['link']);
0 ignored issues
show
Bug introduced by
The method parseValAsFileList() does not exist on Charcoal\Property\PropertyInterface. Did you maybe mean parseVal()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
861
862
        $items = array_merge($files, $links);
863
        $items = array_unique($items);
864
        $items = array_values($items);
865
866
        return $items;
867
    }
868
869
    /**
870
     * Basename of the associated file.
871
     * @return string Basename of file.
872
     */
873
    public function basename()
874
    {
875
        if (!$this->file()) {
876
            return '';
877
        }
878
879
        return basename(strval($this->file()));
880
    }
881
882
    /**
883
     * Retrieve the file label.
884
     *
885
     * @return string|null
886
     */
887
    public function fileLabel()
888
    {
889
        return $this->fileLabel;
890
    }
891
892
    /**
893
     * Retrieve the link label.
894
     *
895
     * @return string|null
896
     */
897
    public function linkLabel()
898
    {
899
        return $this->linkLabel;
900
    }
901
902
    /**
903
     * Retrieve the attached file's size.
904
     *
905
     * @return integer Returns the size of the file in bytes, or FALSE in case of an error.
906
     */
907
    public function fileSize()
908
    {
909
        return $this->fileSize;
910
    }
911
912
    /**
913
     * File type / extension
914
     * @return string File extension.
915
     */
916
    public function fileType()
917
    {
918
        return $this->fileType;
919
    }
920
921
    /**
922
     * Retrieve the embed content.
923
     *
924
     * @return string
925
     */
926
    public function embed()
927
    {
928
        return $this->embed;
929
    }
930
931
    /**
932
     * @return string|\string[]
933
     */
934
    public function categories()
935
    {
936
        return $this->categories;
937
    }
938
939
    /**
940
     * @return ModelInterface|mixed
941
     */
942
    public function presenter()
943
    {
944
        return $this->presenter;
945
    }
946
947
    /**
948
     * @param ModelInterface|mixed $presenter Presenter for Attachment.
949
     * @return self
950
     */
951
    public function setPresenter($presenter)
952
    {
953
        $this->presenter = $presenter;
954
955
        return $this;
956
    }
957
958
    // Events
959
    // =============================================================================
960
961
    /**
962
     * Event called before _deleting_ the attachment.
963
     *
964
     * @see    Charcoal\Source\StorableTrait::preDelete() For the "create" Event.
965
     * @see    Charcoal\Attachment\Traits\AttachmentAwareTrait::removeJoins
966
     * @return boolean
967
     */
968
    public function preDelete()
969
    {
970
        $joinCollection = $this->collectionLoader()
971
            ->reset()
972
            ->setModel(Join::class)
973
            ->addFilter('attachment_id', $this['id'])
974
            ->load();
975
976
        foreach ($joinCollection as $joinModel) {
977
            $joinModel->delete();
978
        }
979
980
        return parent::preDelete();
981
    }
982
983
    // Utilities
984
    // =============================================================================
985
986
    /**
987
     * Set the base URI of the project.
988
     *
989
     * @see    \Charcoal\Admin\Support\setBaseUrl::baseUrl()
990
     * @param  UriInterface $uri The base URI.
991
     * @return self
992
     */
993
    protected function setBaseUrl(UriInterface $uri)
994
    {
995
        $this->baseUrl = $uri;
996
997
        return $this;
998
    }
999
1000
    /**
1001
     * Retrieve the base URI of the project.
1002
     *
1003
     * @throws RuntimeException If the base URI is missing.
1004
     * @return UriInterface|null
1005
     */
1006
    public function baseUrl()
1007
    {
1008
        if (!isset($this->baseUrl)) {
1009
            throw new RuntimeException(sprintf(
1010
                'The base URI is not defined for [%s]',
1011
                get_class($this)
1012
            ));
1013
        }
1014
1015
        return $this->baseUrl;
1016
    }
1017
1018
    /**
1019
     * Prepend the base URI to the given path.
1020
     *
1021
     * @param  string $uri A URI path to wrap.
1022
     * @return UriInterface|null
1023
     */
1024
    public function createAbsoluteUrl($uri)
1025
    {
1026
        if (!isset($uri)) {
1027
            return null;
1028
        }
1029
1030
        $uri = strval($uri);
1031
        if ($this->isRelativeUri($uri)) {
1032
            $parts = parse_url($uri);
1033
            $path  = isset($parts['path']) ? $parts['path'] : '';
1034
            $query = isset($parts['query']) ? $parts['query'] : '';
1035
            $hash  = isset($parts['fragment']) ? $parts['fragment'] : '';
1036
1037
            return $this->baseUrl()->withPath($path)->withQuery($query)->withFragment($hash);
1038
        }
1039
1040
        return $uri;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $uri; (string) is incompatible with the return type documented by Charcoal\Attachment\Obje...ment::createAbsoluteUrl of type Psr\Http\Message\UriInterface|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1041
    }
1042
1043
    /**
1044
     * Prepend the base URI to the given path.
1045
     *
1046
     * @param  string $text A string to parse relative URIs.
1047
     * @return UriInterface|null
1048
     */
1049
    protected function resolveUrls($text)
1050
    {
1051
        static $search;
1052
1053
        if ($search === null) {
1054
            $attr   = [ 'href', 'link', 'url', 'src' ];
1055
            $scheme = [ '../', './', '/', 'data', 'mailto', 'http' ];
1056
1057
            $search = sprintf(
1058
                '(?<=%1$s=")(?!%2$s)(\S+)(?=")',
1059
                implode('="|', array_map('preg_quote', $attr, [ '~' ])),
1060
                implode('|', array_map('preg_quote', $scheme, [ '~' ]))
1061
            );
1062
        }
1063
1064
        $text = preg_replace_callback(
1065
            '~'.$search.'~i',
1066
            function ($matches) {
1067
                return $this->createAbsoluteUrl($matches[1]);
1068
            },
1069
            $text
1070
        );
1071
1072
        return $text;
1073
    }
1074
1075
    /**
1076
     * Determine if the given URI is relative.
1077
     *
1078
     * @see    \Charcoal\Admin\Support\BaseUrlTrait::isRelativeUri()
1079
     * @param  string $uri A URI path to test.
1080
     * @return boolean
1081
     */
1082
    protected function isRelativeUri($uri)
1083
    {
1084
        if ($uri && !parse_url($uri, PHP_URL_SCHEME)) {
1085
            if (!in_array($uri[0], [ '/', '#', '?' ])) {
1086
                return true;
1087
            }
1088
        }
1089
1090
        return false;
1091
    }
1092
1093
    /**
1094
     * Set a model collection loader.
1095
     *
1096
     * @param  CollectionLoader $loader The collection loader.
1097
     * @return self
1098
     */
1099
    protected function setCollectionLoader(CollectionLoader $loader)
1100
    {
1101
        $this->collectionLoader = $loader;
1102
1103
        return $this;
1104
    }
1105
1106
    /**
1107
     * Retrieve the model collection loader.
1108
     *
1109
     * @throws Exception If the collection loader was not previously set.
1110
     * @return CollectionLoader
1111
     */
1112
    public function collectionLoader()
1113
    {
1114
        if (!isset($this->collectionLoader)) {
1115
            throw new Exception(sprintf(
1116
                'Collection Loader is not defined for "%s"',
1117
                get_class($this)
1118
            ));
1119
        }
1120
1121
        return $this->collectionLoader;
1122
    }
1123
}
1124