Test Setup Failed
Push — master ( 6592af...c06444 )
by Chauncey
08:19
created

ImageProperty::batchEffects()   B

Complexity

Conditions 11
Paths 3

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 7.3166
c 0
b 0
f 0
cc 11
nc 3
nop 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * Provides the accepted mimetypes for the image properties.
266
     *
267
     * Overrides FileProperty's getAcceptedMimetypes() method.
268
     *
269
     * @return string[]
270
     */
271
    public function getAcceptedMimetypes()
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
     * Generate the file extension from the property's value.
286
     *
287
     * @param  string $file The file to parse.
288
     * @return string The extension based on the MIME type.
289
     */
290
    public function generateExtension($file = null)
291
    {
292 View Code Duplication
        if (is_string($file)) {
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...
293
            if (in_array($file, $this['acceptedMimetypes'])) {
294
                $mime = $file;
295
            } else {
296
                $mime = $this->getMimetypeFor($file);
297
            }
298
        } else {
299
            $mime = $this->getMimetype();
300
        }
301
302
        switch ($mime) {
303
            case 'image/gif':
304
                return 'gif';
305
306
            case 'image/jpg':
307
            case 'image/jpeg':
308
            case 'image/pjpeg':
309
                return 'jpg';
310
311
            case 'image/png':
312
                return 'png';
313
314
            case 'image/svg+xml':
315
                return 'svg';
316
317
            case 'image/webp':
318
                return 'webp';
319
        }
320
    }
321
322
    /**
323
     * @param mixed $val The value, at time of saving.
324
     * @return mixed
325
     */
326
    public function save($val)
327
    {
328
        $val = parent::save($val);
329
330
        if ($this->canApplyEffects('save')) {
331
            $val = $this->processEffects($val);
332
        }
333
334
        return $val;
335
    }
336
337
    /**
338
     * Apply effects to the uploaded data URI(s).
339
     *
340
     * @see    FileProperty::fileUpload()
341
     * @param  string $fileData The file data, raw.
342
     * @return string
343
     */
344 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...
345
    {
346
        $target = parent::dataUpload($fileData);
347
348
        if ($this->canApplyEffects('upload')) {
349
            $target = $this->processEffects($target);
350
        }
351
352
        return $target;
353
    }
354
355
    /**
356
     * Apply effects to the uploaded file(s).
357
     *
358
     * @see    FileProperty::fileUpload()
359
     * @param  array $fileData The file data to upload.
360
     * @return string
361
     */
362 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...
363
    {
364
        $target = parent::fileUpload($fileData);
365
366
        if ($this->canApplyEffects('upload')) {
367
            $target = $this->processEffects($target);
368
        }
369
370
        return $target;
371
    }
372
373
    /**
374
     * Set an image factory.
375
     *
376
     * @param  ImageFactory $factory The image factory, to manipulate images.
377
     * @return self
378
     */
379
    protected function setImageFactory(ImageFactory $factory)
380
    {
381
        $this->imageFactory = $factory;
382
383
        return $this;
384
    }
385
386
    /**
387
     * Create an image factory.
388
     *
389
     * @return ImageFactory
390
     */
391
    protected function createImageFactory()
392
    {
393
        return new ImageFactory();
394
    }
395
396
    /**
397
     * Create an image.
398
     *
399
     * @return ImageInterface
400
     */
401
    protected function createImage()
402
    {
403
        return $this->imageFactory()->create($this['driverType']);
404
    }
405
406
    /**
407
     * @return array
408
     */
409
    protected function batchEffects()
410
    {
411
        $effects = $this['effects'];
412
        $grouped = [];
413
        if ($effects) {
414
            $blueprint = [
415
                'effects' => [],
416
                'save'    => true,
417
                'rename'  => null,
418
                'reset'   => false,
419
                'copy'    => null,
420
            ];
421
            $fxGroup   = $blueprint;
422
            foreach ($effects as $effect) {
423
                if (isset($effect['type']) && $effect['type'] === 'condition') {
424
                    $grouped[] = array_merge(
425
                        [
426
                            'condition' => null,
427
                            'ignore'    => null,
428
                            'extension' => null,
429
                            'mimetype'  => null,
430
                        ],
431
                        $effect
432
                    );
433
                } elseif (isset($effect['type']) && $effect['type'] === 'save') {
434
                    if (isset($effect['rename'])) {
435
                        $fxGroup['rename'] = $effect['rename'];
436
                    }
437
                    if (isset($effect['copy'])) {
438
                        $fxGroup['copy'] = $effect['copy'];
439
                    }
440
                    if (isset($effect['reset'])) {
441
                        $fxGroup['reset'] = $effect['reset'];
442
                    }
443
444
                    $grouped[] = $fxGroup;
445
446
                    $fxGroup = $blueprint;
447
                } else {
448
                    $fxGroup['effects'][] = $effect;
449
                }
450
            }
451
452
            if (empty($grouped)) {
453
                $grouped[] = $fxGroup;
454
            }
455
        }
456
457
        return $grouped;
458
    }
459
460
    /**
461
     * Process the property's effects on the given image.
462
     *
463
     * @param  string              $value   The target to apply effects on.
464
     * @param  array               $effects The effects to apply on the target.
465
     * @param  ImageInterface|null $image   Optional. The image for processing.
466
     * @throws InvalidArgumentException If the $value is not a string.
467
     * @return mixed Returns the processed target or NULL.
468
     */
469
    private function processEffectsOne($value, array $effects = null, ImageInterface $image = null)
470
    {
471
        if ($value === null || $value === '') {
472
            return null;
473
        }
474
475
        if (!is_string($value)) {
476
            throw new InvalidArgumentException(sprintf(
477
                'Target image must be a string, received %s',
478
                (is_object($value) ? get_class($value) : gettype($value))
479
            ));
480
        }
481
482
        if ($image === null) {
483
            $image = $this->createImage();
484
        }
485
486
        if ($effects === null) {
487
            $effects = $this->batchEffects();
488
        }
489
490
        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...
491
            $basePath = $this->basePath();
492
493
            $isAbsolute = false;
494
            if (null !== parse_url($value, PHP_URL_HOST)) {
495
                $isAbsolute = true;
496
            }
497
498
            // @todo Save original file here
499
            $valuePath = ($isAbsolute ? '' : $basePath);
500
            $image->open($valuePath.$value);
501
            $target = null;
502
            if ($isAbsolute) {
503
                $target = $basePath.$this['uploadPath'].pathinfo($value, PATHINFO_BASENAME);
504
            }
505
506
            foreach ($effects as $fxGroup) {
507
                if (isset($fxGroup['type']) && !empty($fxGroup['condition'])) {
508
                    if ($fxGroup['condition'] === 'ignore') {
509
                        switch ($fxGroup['ignore']) {
510
                            case 'extension':
511
                                $type = pathinfo($value, PATHINFO_EXTENSION);
512
                                if (in_array($type, (array)$fxGroup['extension'])) {
513
                                    break 2;
514
                                }
515
                                break;
516
517
                            case 'mimetype':
518
                                $type = $this->getMimetypeFor($value);
519
                                if (in_array($type, (array)$fxGroup['mimetype'])) {
520
                                    break 2;
521
                                }
522
                                break;
523
                        }
524
                    } else {
525
                        if (is_string($fxGroup['condition'])) {
526
                            $this->logger->warning(sprintf(
527
                                '[Image Property] Unsupported conditional effect: \'%s\'',
528
                                $fxGroup['condition']
529
                            ));
530
                        } else {
531
                            $this->logger->warning(sprintf(
532
                                '[Image Property] Invalid conditional effect: \'%s\'',
533
                                gettype($fxGroup['condition'])
534
                            ));
535
                        }
536
                    }
537
                } elseif ($fxGroup['save']) {
538
                    $rename = $fxGroup['rename'];
539
                    $copy   = $fxGroup['copy'];
540
541
                    $doRename = false;
542
                    $doCopy   = false;
543
                    $doSave   = true;
544
545
                    if ($rename || $copy) {
546 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...
547
                            $copy   = $this->renderFileRenamePattern(($target ?: $value), $copy);
548
                            $exists = $this->fileExists($basePath.$copy);
549
                            $doCopy = ($copy && ($this['overwrite'] || !$exists));
550
                        }
551
552 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...
553
                            $value    = $this->renderFileRenamePattern(($target ?: $value), $rename);
554
                            $exists   = $this->fileExists($basePath.$value);
555
                            $doRename = ($value && ($this['overwrite'] || !$exists));
556
                        }
557
558
                        $doSave = ($doCopy || $doRename);
559
                    }
560
561
                    if ($doSave) {
562
                        if ($fxGroup['effects']) {
563
                            $image->setEffects($fxGroup['effects']);
564
                            $image->process();
565
                        }
566
567
                        if ($rename || $copy) {
568
                            if ($doCopy) {
569
                                $image->save($valuePath.$copy);
570
                            }
571
572
                            if ($doRename) {
573
                                $image->save($valuePath.$value);
574
                            }
575
                        } else {
576
                            $image->save($target ?: $valuePath.$value);
577
                        }
578
                    }
579
                }
580
                // reset to default image allow starting effects chains from original image.
581
                if ($fxGroup['reset']) {
582
                    $image = $image->open($valuePath.$value);
583
                }
584
            }
585
        }
586
587
        return $value;
588
    }
589
}
590