Test Setup Failed
Push — master ( 2f148a...436d74 )
by Chauncey
01:04 queued 11s
created

ImageProperty::resolveExtensionFromMimeType()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 23

Duplication

Lines 23
Ratio 100 %

Importance

Changes 0
Metric Value
dl 23
loc 23
rs 8.4444
c 0
b 0
f 0
cc 8
nc 8
nop 1
1
<?php
2
3
namespace Charcoal\Property;
4
5
use InvalidArgumentException;
6
use OutOfBoundsException;
7
8
// From 'charcoal-image'
9
use Charcoal\Image\ImageFactory;
10
use Charcoal\Image\ImageInterface;
11
12
// From 'charccoal-translator'
13
use Charcoal\Translator\Translation;
14
15
// From 'charcoal-property'
16
use Charcoal\Property\FileProperty;
17
18
/**
19
 * Image Property.
20
 *
21
 * The image property is a specialized file property that stores image file.
22
 */
23
class ImageProperty extends FileProperty
24
{
25
    const DEFAULT_DRIVER_TYPE = 'imagick';
26
27
    const EFFECTS_EVENT_SAVE    = 'save';
28
    const EFFECTS_EVENT_NEVER   = 'never';
29
    const EFFECTS_EVENT_UPLOAD  = 'upload';
30
    const DEFAULT_APPLY_EFFECTS = self::EFFECTS_EVENT_SAVE;
31
32
    /**
33
     * One or more effects to apply on the image.
34
     *
35
     * @var array
36
     */
37
    private $effects = [];
38
39
    /**
40
     * Whether to apply any effects on the uploaded image.
41
     *
42
     * @var mixed
43
     */
44
    private $applyEffects = self::DEFAULT_APPLY_EFFECTS;
45
46
    /**
47
     * The type of image processing engine.
48
     *
49
     * @var string
50
     */
51
    private $driverType = self::DEFAULT_DRIVER_TYPE;
52
53
    /**
54
     * Internal storage of the image factory instance.
55
     *
56
     * @var ImageFactory
57
     */
58
    private $imageFactory;
59
60
    /**
61
     * @return string
62
     */
63
    public function type()
64
    {
65
        return 'image';
66
    }
67
68
    /**
69
     * Retrieve the image factory.
70
     *
71
     * @return ImageFactory
72
     */
73
    public function imageFactory()
74
    {
75
        if ($this->imageFactory === null) {
76
            $this->imageFactory = $this->createImageFactory();
77
        }
78
79
        return $this->imageFactory;
80
    }
81
82
    /**
83
     * Set the name of the property's image processing driver.
84
     *
85
     * @param  string $type The processing engine.
86
     * @throws InvalidArgumentException If the drive type is not a string.
87
     * @return ImageProperty Chainable
88
     */
89 View Code Duplication
    public function setDriverType($type)
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...
90
    {
91
        if (!is_string($type)) {
92
            throw new InvalidArgumentException(sprintf(
93
                'Image driver type must be a string, received %s',
94
                (is_object($type) ? get_class($type) : gettype($type))
95
            ));
96
        }
97
98
        $this->driverType = $type;
99
100
        return $this;
101
    }
102
103
    /**
104
     * Retrieve the name of the property's image processing driver.
105
     *
106
     * @return string
107
     */
108
    public function getDriverType()
109
    {
110
        return $this->driverType;
111
    }
112
113
    /**
114
     * Set whether effects should be applied.
115
     *
116
     * @param  mixed $event When to apply affects.
117
     * @throws OutOfBoundsException If the effects event does not exist.
118
     * @return ImageProperty Chainable
119
     */
120
    public function setApplyEffects($event)
121
    {
122
        if ($event === false) {
123
            $this->applyEffects = self::EFFECTS_EVENT_NEVER;
124
            return $this;
125
        }
126
127
        if ($event === null || $event === '') {
128
            $this->applyEffects = self::EFFECTS_EVENT_SAVE;
129
            return $this;
130
        }
131
132 View Code Duplication
        if (!in_array($event, $this->acceptedEffectsEvents())) {
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...
133
            if (!is_string($event)) {
134
                $event = (is_object($event) ? get_class($event) : gettype($event));
135
            }
136
            throw new OutOfBoundsException(sprintf(
137
                'Unsupported image property event "%s" provided',
138
                $event
139
            ));
140
        }
141
142
        $this->applyEffects = $event;
143
144
        return $this;
145
    }
146
147
    /**
148
     * Determine if effects should be applied.
149
     *
150
     * @return string Returns the property's condition on effects.
151
     */
152
    public function getApplyEffects()
153
    {
154
        return $this->applyEffects;
155
    }
156
157
    /**
158
     * Determine if effects should be applied.
159
     *
160
     * @param  string|boolean $event A specific event to check or a global flag to set.
161
     * @throws OutOfBoundsException If the effects event does not exist.
162
     * @return mixed Returns TRUE or FALSE if the property applies effects for the given event.
163
     */
164
    public function canApplyEffects($event)
165
    {
166 View Code Duplication
        if (!in_array($event, $this->acceptedEffectsEvents())) {
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...
167
            if (!is_string($event)) {
168
                $event = (is_object($event) ? get_class($event) : gettype($event));
169
            }
170
            throw new OutOfBoundsException(sprintf(
171
                'Unsupported image property event "%s" provided',
172
                $event
173
            ));
174
        }
175
176
        return $this->applyEffects === $event;
177
    }
178
179
    /**
180
     * Retrieve the supported events where effects can be applied.
181
     *
182
     * @return array
183
     */
184
    public function acceptedEffectsEvents()
185
    {
186
        return [
187
            self::EFFECTS_EVENT_UPLOAD,
188
            self::EFFECTS_EVENT_SAVE,
189
            self::EFFECTS_EVENT_NEVER,
190
        ];
191
    }
192
193
    /**
194
     * Set (reset, in fact) the image effects.
195
     *
196
     * @param array $effects The effects to set to the image.
197
     * @return ImageProperty Chainable
198
     */
199
    public function setEffects(array $effects)
200
    {
201
        $this->effects = [];
202
        foreach ($effects as $effect) {
203
            $this->addEffect($effect);
204
        }
205
        return $this;
206
    }
207
208
    /**
209
     * @param mixed $effect An image effect.
210
     * @return ImageProperty Chainable
211
     */
212
    public function addEffect($effect)
213
    {
214
        $this->effects[] = $effect;
215
        return $this;
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    public function getEffects()
222
    {
223
        return $this->effects;
224
    }
225
226
    /**
227
     * Process the property's effects on the given image(s).
228
     *
229
     * @param  mixed               $value   The target(s) to apply effects on.
230
     * @param  array               $effects The effects to apply on the target.
231
     * @param  ImageInterface|null $image   Optional. The image for processing.
232
     * @return mixed Returns the given images. Depending on the effects applied,
233
     *     certain images might be renamed.
234
     */
235
    public function processEffects($value, array $effects = null, ImageInterface $image = null)
236
    {
237
        $value = $this->parseVal($value);
238
239
        if ($value instanceof Translation) {
240
            $value = $value->data();
241
        }
242
243
        if ($effects === null) {
244
            $effects = $this->batchEffects();
245
        }
246
247
        if ($effects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $effects 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...
248
            if ($image === null) {
249
                $image = $this->createImage();
250
            }
251
252
            if (is_array($value)) {
253
                foreach ($value as &$val) {
254
                    $val = $this->processEffectsOne($val, $effects, $image);
255
                }
256
            } else {
257
                $value = $this->processEffectsOne($value, $effects, $image);
258
            }
259
        }
260
261
        return $value;
262
    }
263
264
    /**
265
     * Retrieves the default list of acceptable MIME types for uploaded files.
266
     *
267
     * This method should be overriden.
268
     *
269
     * @return string[]
270
     */
271 View Code Duplication
    public function getDefaultAcceptedMimetypes()
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...
272
    {
273
        return [
274
            'image/gif',
275
            'image/jpg',
276
            'image/jpeg',
277
            'image/pjpeg',
278
            'image/png',
279
            'image/svg+xml',
280
            'image/webp',
281
        ];
282
    }
283
284
    /**
285
     * Resolve the file extension from the given MIME type.
286
     *
287
     * @param  string $type The MIME type to resolve.
288
     * @return string|null The extension based on the MIME type.
289
     */
290 View Code Duplication
    protected function resolveExtensionFromMimeType($type)
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...
291
    {
292
        switch ($type) {
293
            case 'image/gif':
294
                return 'gif';
295
296
            case 'image/jpg':
297
            case 'image/jpeg':
298
            case 'image/pjpeg':
299
                return 'jpg';
300
301
            case 'image/png':
302
                return 'png';
303
304
            case 'image/svg+xml':
305
                return 'svg';
306
307
            case 'image/webp':
308
                return 'webp';
309
        }
310
311
        return null;
312
    }
313
314
    /**
315
     * @param mixed $val The value, at time of saving.
316
     * @return mixed
317
     */
318
    public function save($val)
319
    {
320
        $val = parent::save($val);
321
322
        if ($this->canApplyEffects('save')) {
323
            $val = $this->processEffects($val);
324
        }
325
326
        return $val;
327
    }
328
329
    /**
330
     * Apply effects to the uploaded data URI(s).
331
     *
332
     * @see    FileProperty::fileUpload()
333
     * @param  string $fileData The file data, raw.
334
     * @return string
335
     */
336 View Code Duplication
    public function dataUpload($fileData)
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...
337
    {
338
        $target = parent::dataUpload($fileData);
339
340
        if ($this->canApplyEffects('upload')) {
341
            $target = $this->processEffects($target);
342
        }
343
344
        return $target;
345
    }
346
347
    /**
348
     * Apply effects to the uploaded file(s).
349
     *
350
     * @see    FileProperty::fileUpload()
351
     * @param  array $fileData The file data to upload.
352
     * @return string
353
     */
354 View Code Duplication
    public function fileUpload(array $fileData)
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...
355
    {
356
        $target = parent::fileUpload($fileData);
357
358
        if ($this->canApplyEffects('upload')) {
359
            $target = $this->processEffects($target);
360
        }
361
362
        return $target;
363
    }
364
365
    /**
366
     * Set an image factory.
367
     *
368
     * @param  ImageFactory $factory The image factory, to manipulate images.
369
     * @return self
370
     */
371
    protected function setImageFactory(ImageFactory $factory)
372
    {
373
        $this->imageFactory = $factory;
374
375
        return $this;
376
    }
377
378
    /**
379
     * Create an image factory.
380
     *
381
     * @return ImageFactory
382
     */
383
    protected function createImageFactory()
384
    {
385
        return new ImageFactory();
386
    }
387
388
    /**
389
     * Create an image.
390
     *
391
     * @return ImageInterface
392
     */
393
    protected function createImage()
394
    {
395
        return $this->imageFactory()->create($this['driverType']);
396
    }
397
398
    /**
399
     * @return array
400
     */
401
    protected function batchEffects()
402
    {
403
        $effects = $this['effects'];
404
        $grouped = [];
405
        if ($effects) {
406
            $blueprint = [
407
                'effects' => [],
408
                'save'    => true,
409
                'rename'  => null,
410
                'reset'   => false,
411
                'copy'    => null,
412
            ];
413
            $fxGroup   = $blueprint;
414
            foreach ($effects as $effect) {
415
                if (isset($effect['type']) && $effect['type'] === 'condition') {
416
                    $grouped[] = array_merge(
417
                        [
418
                            'condition' => null,
419
                            'ignore'    => null,
420
                            'extension' => null,
421
                            'mimetype'  => null,
422
                        ],
423
                        $effect
424
                    );
425
                } elseif (isset($effect['type']) && $effect['type'] === 'save') {
426
                    if (isset($effect['rename'])) {
427
                        $fxGroup['rename'] = $effect['rename'];
428
                    }
429
                    if (isset($effect['copy'])) {
430
                        $fxGroup['copy'] = $effect['copy'];
431
                    }
432
                    if (isset($effect['reset'])) {
433
                        $fxGroup['reset'] = $effect['reset'];
434
                    }
435
436
                    $grouped[] = $fxGroup;
437
438
                    $fxGroup = $blueprint;
439
                } else {
440
                    $fxGroup['effects'][] = $effect;
441
                }
442
            }
443
444
            if (empty($grouped)) {
445
                $grouped[] = $fxGroup;
446
            }
447
        }
448
449
        return $grouped;
450
    }
451
452
    /**
453
     * Process the property's effects on the given image.
454
     *
455
     * @param  string              $value   The target to apply effects on.
456
     * @param  array               $effects The effects to apply on the target.
457
     * @param  ImageInterface|null $image   Optional. The image for processing.
458
     * @throws InvalidArgumentException If the $value is not a string.
459
     * @return mixed Returns the processed target or NULL.
460
     */
461
    private function processEffectsOne($value, array $effects = null, ImageInterface $image = null)
462
    {
463
        if ($value === null || $value === '') {
464
            return null;
465
        }
466
467
        if (!is_string($value)) {
468
            throw new InvalidArgumentException(sprintf(
469
                'Target image must be a string, received %s',
470
                (is_object($value) ? get_class($value) : gettype($value))
471
            ));
472
        }
473
474
        if (!$this->fileExists($value)) {
475
            return $value;
476
        }
477
478
        if ($image === null) {
479
            $image = $this->createImage();
480
        }
481
482
        if ($effects === null) {
483
            $effects = $this->batchEffects();
484
        }
485
486
        if ($effects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $effects 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...
487
            $basePath = $this->basePath();
488
489
            $isAbsolute = false;
490
            if (null !== parse_url($value, PHP_URL_HOST)) {
491
                $isAbsolute = true;
492
            }
493
494
            // @todo Save original file here
495
            $valuePath = ($isAbsolute ? '' : $basePath);
496
            $image->open(static::normalizePath($valuePath.$value));
497
            $target = null;
498
            if ($isAbsolute) {
499
                $target = static::normalizePath($basePath.$this['uploadPath'].pathinfo($value, PATHINFO_BASENAME));
500
            }
501
502
            foreach ($effects as $fxGroup) {
503
                if (isset($fxGroup['type']) && !empty($fxGroup['condition'])) {
504
                    if ($fxGroup['condition'] === 'ignore') {
505
                        switch ($fxGroup['ignore']) {
506
                            case 'extension':
507
                                $type = pathinfo($value, PATHINFO_EXTENSION);
508
                                if (in_array($type, (array)$fxGroup['extension'])) {
509
                                    break 2;
510
                                }
511
                                break;
512
513
                            case 'mimetype':
514
                                $type = $this->getMimetypeFor($value);
515
                                if (in_array($type, (array)$fxGroup['mimetype'])) {
516
                                    break 2;
517
                                }
518
                                break;
519
                        }
520
                    } else {
521
                        if (is_string($fxGroup['condition'])) {
522
                            $this->logger->warning(sprintf(
523
                                '[Image Property] Unsupported conditional effect: \'%s\'',
524
                                $fxGroup['condition']
525
                            ));
526
                        } else {
527
                            $this->logger->warning(sprintf(
528
                                '[Image Property] Invalid conditional effect: \'%s\'',
529
                                gettype($fxGroup['condition'])
530
                            ));
531
                        }
532
                    }
533
                } elseif ($fxGroup['save']) {
534
                    $rename = $fxGroup['rename'];
535
                    $copy   = $fxGroup['copy'];
536
537
                    $doRename = false;
538
                    $doCopy   = false;
539
                    $doSave   = true;
540
541
                    if ($rename || $copy) {
542 View Code Duplication
                        if ($copy) {
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...
543
                            $copy   = $this->renderFileRenamePattern(($target ?: $value), $copy);
544
                            $exists = $this->fileExists(static::normalizePath($basePath.$copy));
545
                            $doCopy = ($copy && ($this['overwrite'] || !$exists));
546
                        }
547
548 View Code Duplication
                        if ($rename) {
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...
549
                            $value    = $this->renderFileRenamePattern(($target ?: $value), $rename);
550
                            $exists   = $this->fileExists(static::normalizePath($basePath.$value));
551
                            $doRename = ($value && ($this['overwrite'] || !$exists));
552
                        }
553
554
                        $doSave = ($doCopy || $doRename);
555
                    }
556
557
                    if ($doSave) {
558
                        if ($fxGroup['effects']) {
559
                            $image->setEffects($fxGroup['effects']);
560
                            $image->process();
561
                        }
562
563
                        if ($rename || $copy) {
564
                            if ($doCopy) {
565
                                $image->save(static::normalizePath($valuePath.$copy));
566
                            }
567
568
                            if ($doRename) {
569
                                $image->save(static::normalizePath($valuePath.$value));
570
                            }
571
                        } else {
572
                            $image->save($target ?: static::normalizePath($valuePath.$value));
573
                        }
574
                    }
575
                }
576
                // reset to default image allow starting effects chains from original image.
577
                if ($fxGroup['reset']) {
578
                    $image = $image->open(static::normalizePath($valuePath.$value));
579
                }
580
            }
581
        }
582
583
        return $value;
584
    }
585
}
586