Passed
Push — master ( 8298cf...50f59c )
by Bjørn
02:59
created

AbstractConverter::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3.0026

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 3
nop 4
dl 0
loc 21
ccs 14
cts 15
cp 0.9333
crap 3.0026
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
// TODO:
4
// Read this: https://sourcemaking.com/design_patterns/strategy
5
6
namespace WebPConvert\Convert\Converters;
7
8
use WebPConvert\Convert\Exceptions\ConversionFailedException;
9
use WebPConvert\Exceptions\WebPConvertException;
10
use WebPConvert\Convert\Converters\BaseTraits\AutoQualityTrait;
11
use WebPConvert\Convert\Converters\BaseTraits\DestinationPreparationTrait;
12
use WebPConvert\Convert\Converters\BaseTraits\LoggerTrait;
13
use WebPConvert\Convert\Converters\BaseTraits\OptionsTrait;
14
use WebPConvert\Convert\Converters\BaseTraits\SourceValidationTrait;
15
use WebPConvert\Convert\Converters\BaseTraits\WarningLoggerTrait;
16
use WebPConvert\Loggers\BaseLogger;
17
18
use ImageMimeTypeGuesser\ImageMimeTypeGuesser;
19
20
/**
21
 * Base for all converter classes.
22
 *
23
 * @package    WebPConvert
24
 * @author     Bjørn Rosell <[email protected]>
25
 * @since      Class available since Release 2.0.0
26
 */
27
abstract class AbstractConverter
28
{
29
    use AutoQualityTrait;
30
    use OptionsTrait;
31
    use WarningLoggerTrait;
32
    use DestinationPreparationTrait;
33
    use SourceValidationTrait;
34
    use LoggerTrait;
35
36
    /**
37
     * The actual conversion is be done by a concrete converter extending this class.
38
     *
39
     * At the stage this method is called, the abstract converter has taken preparational steps.
40
     * - It has created the destination folder (if neccesary)
41
     * - It has checked the input (valid mime type)
42
     * - It has set up an error handler, mostly in order to catch and log warnings during the doConvert fase
43
     *
44
     * Note: This method is not meant to be called from the outside. Use the static *convert* method for converting
45
     *       or, if you wish, create an instance with ::createInstance() and then call ::doConvert()
46
     *
47
     * @throws ConversionFailedException in case conversion failed in an antipiciated way (or subclass)
48
     * @throws \Exception in case conversion failed in an unantipiciated way
49
     */
50
    abstract protected function doActualConvert();
51
52
    /**
53
     * Whether or not the converter supports lossless encoding (even for jpegs)
54
     *
55
     * PS: Converters that supports lossless encoding all use the EncodingAutoTrait, which
56
     * overrides this function.
57
     *
58
     * @return  boolean  Whether the converter supports lossless encoding (even for jpegs).
59
     */
60
    public function supportsLossless()
61
    {
62
        return false;
63
    }
64
65
    /** @var string  The filename of the image to convert (complete path) */
66
    protected $source;
67
68
    /** @var string  Where to save the webp (complete path) */
69
    protected $destination;
70
71
    /** @var string|false|null  Where to save the webp (complete path) */
72
    private $sourceMimeType;
73
74
    /**
75
     * Check basis operationality
76
     *
77
     * Converters may override this method for the purpose of performing basic operationaly checks. It is for
78
     * running general operation checks for a conversion method.
79
     * If some requirement is not met, it should throw a ConverterNotOperationalException (or subtype)
80
     *
81
     * The method is called internally right before calling doActualConvert() method.
82
     * - It SHOULD take options into account when relevant. For example, a missing api key for a
83
     *   cloud converter should be detected here
84
     * - It should NOT take the actual filename into consideration, as the purpose is *general*
85
     *   For that pupose, converters should override checkConvertability
86
     *   Also note that doConvert method is allowed to throw ConverterNotOperationalException too.
87
     *
88
     * @return  void
89
     */
90 2
    public function checkOperationality()
91
    {
92 2
    }
93
94
    /**
95
     * Converters may override this for the purpose of performing checks on the concrete file.
96
     *
97
     * This can for example be used for rejecting big uploads in cloud converters or rejecting unsupported
98
     * image types.
99
     *
100
     * @return  void
101
     */
102 5
    public function checkConvertability()
103
    {
104 5
    }
105
106
    /**
107
     * Constructor.
108
     *
109
     * @param   string  $source              path to source file
110
     * @param   string  $destination         path to destination
111
     * @param   array   $options (optional)  options for conversion
112
     * @param   BaseLogger $logger (optional)
113
     */
114 31
    public function __construct($source, $destination, $options = [], $logger = null)
115
    {
116 31
        $this->source = $source;
117 31
        $this->destination = $destination;
118
119 31
        $this->setLogger($logger);
120 31
        $this->setProvidedOptions($options);
121
122 31
        if (!isset($this->options['_skip_input_check'])) {
123 31
            $this->log('WebP Convert 2.0.0', 'italic');
124 31
            $this->logLn(' ignited.');
125 31
            $this->logLn('- PHP version: ' . phpversion());
126 31
            if (isset($_SERVER['SERVER_SOFTWARE'])) {
127
                $this->logLn('- Server software: ' . $_SERVER['SERVER_SOFTWARE']);
128
            }
129 31
            $this->logLn('');
130 31
            $this->logLn(self::getConverterDisplayName() . ' converter ignited');
131
        }
132
133 31
        $this->checkSourceExists();
134 30
        $this->checkSourceMimeType();
135 30
    }
136
137
    /**
138
     * Get source.
139
     *
140
     * @return string  The source.
141
     */
142 31
    public function getSource()
143
    {
144 31
        return $this->source;
145
    }
146
147
    /**
148
     * Get destination.
149
     *
150
     * @return string  The destination.
151
     */
152 12
    public function getDestination()
153
    {
154 12
        return $this->destination;
155
    }
156
157
    /**
158
     * Set destination.
159
     *
160
     * @param   string  $destination         path to destination
161
     * @return string  The destination.
162
     */
163 1
    public function setDestination($destination)
164
    {
165 1
        $this->destination = $destination;
166 1
    }
167
168
169
    /**
170
     *  Get converter name for display (defaults to the class name (short)).
171
     *
172
     *  Converters can override this.
173
     *
174
     * @return string  A display name, ie "Gd"
175
     */
176 31
    protected static function getConverterDisplayName()
177
    {
178
        // https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name/25308464
179 31
        return substr(strrchr('\\' . static::class, '\\'), 1);
180
    }
181
182
183
    /**
184
     *  Get converter id (defaults to the class name lowercased)
185
     *
186
     *  Converters can override this.
187
     *
188
     * @return string  A display name, ie "Gd"
189
     */
190 31
    protected static function getConverterId()
191
    {
192 31
        return strtolower(self::getConverterDisplayName());
193
    }
194
195
196
    /**
197
     * Create an instance of this class
198
     *
199
     * @param  string  $source       The path to the file to convert
200
     * @param  string  $destination  The path to save the converted file to
201
     * @param  array   $options      (optional)
202
     * @param  \WebPConvert\Loggers\BaseLogger   $logger       (optional)
203
     *
204
     * @return static
205
     */
206 20
    public static function createInstance($source, $destination, $options = [], $logger = null)
207
    {
208
209 20
        return new static($source, $destination, $options, $logger);
210
    }
211
212 3
    protected function logReduction($source, $destination)
213
    {
214 3
        $sourceSize = filesize($source);
215 3
        $destSize = filesize($destination);
216 3
        $this->log(round(($sourceSize - $destSize)/$sourceSize * 100) . '% ');
217 3
        if ($sourceSize < 10000) {
218 1
            $this->logLn('(went from ' . strval($sourceSize) . ' bytes to '. strval($destSize) . ' bytes)');
219
        } else {
220 3
            $this->logLn('(went from ' . round($sourceSize/1024) . ' kb to ' . round($destSize/1024) . ' kb)');
221
        }
222 3
    }
223
224
    /**
225
     * Run conversion.
226
     *
227
     * @return void
228
     */
229 12
    private function doConvertImplementation()
230
    {
231 12
        $beginTime = microtime(true);
232
233 12
        $this->activateWarningLogger();
234
235 12
        $this->checkOptions();
236
237
        // Prepare destination folder
238 12
        $this->createWritableDestinationFolder();
239 11
        $this->removeExistingDestinationIfExists();
240
241 11
        if (!isset($this->options['_skip_input_check'])) {
242
            // Run basic input validations (if source exists and if file extension is valid)
243 11
            $this->checkSourceExists();
244 11
            $this->checkSourceMimeType();
245
246
            // Check that a file can be written to destination
247 11
            $this->checkDestinationWritable();
248
        }
249
250 11
        $this->checkOperationality();
251 6
        $this->checkConvertability();
252
253 6
        if ($this->options['log-call-arguments']) {
254
            $this->logOptions();
255
            $this->logLn('');
256
        }
257
258 6
        $this->runActualConvert();
259
260 3
        $source = $this->source;
261 3
        $destination = $this->destination;
262
263 3
        if (!@file_exists($destination)) {
264
            throw new ConversionFailedException('Destination file is not there: ' . $destination);
265 3
        } elseif (@filesize($destination) === 0) {
266
            unlink($destination);
267
            throw new ConversionFailedException('Destination file was completely empty');
268
        } else {
269 3
            if (!isset($this->options['_suppress_success_message'])) {
270 3
                $this->ln();
271 3
                $this->log('Converted image in ' . round((microtime(true) - $beginTime) * 1000) . ' ms');
272
273 3
                $sourceSize = @filesize($source);
274 3
                if ($sourceSize !== false) {
275 3
                    $this->log(', reducing file size with ');
276 3
                    $this->logReduction($source, $destination);
277
                }
278
            }
279
        }
280
281 3
        $this->deactivateWarningLogger();
282 3
    }
283
284
    //private function logEx
285
    /**
286
     * Start conversion.
287
     *
288
     * Usually you would rather call the static convert method, but alternatively you can call
289
     * call ::createInstance to get an instance and then ::doConvert().
290
     *
291
     * @return void
292
     */
293 12
    public function doConvert()
294
    {
295
        try {
296
            //trigger_error('hello', E_USER_ERROR);
297 12
            $this->doConvertImplementation();
298 9
        } catch (WebPConvertException $e) {
299 9
            $this->logLn('');
300
            /*
301
            if (isset($e->description) && ($e->description != '')) {
302
                $this->log('Error: ' . $e->description . '. ', 'bold');
303
            } else {
304
                $this->log('Error: ', 'bold');
305
            }
306
            */
307 9
            $this->log('Error: ', 'bold');
308 9
            $this->logLn($e->getMessage(), 'bold');
309 9
            throw $e;
310
        } catch (\Exception $e) {
311
            $className = get_class($e);
312
313
            $classNameParts = explode("\\", $className);
314
            $shortClassName = array_pop($classNameParts);
315
316
            $this->logLn('');
317
            $this->logLn($shortClassName . ' thrown in ' . $e->getFile() . ':' . $e->getLine(), 'bold');
318
            $this->logLn('Message: "' . $e->getMessage() . '"', 'bold');
319
            //$this->logLn('Exception class: ' . $className);
320
321
            $this->logLn('Trace:');
322
            foreach ($e->getTrace() as $trace) {
323
                //$this->logLn(print_r($trace, true));
324
                if (isset($trace['file']) && isset($trace['line'])) {
325
                    $this->logLn(
326
                        $trace['file'] . ':' . $trace['line']
327
                    );                    
328
                }
329
            }
330
            throw $e;
331
        } /*catch (\Error $e) {
332
            $this->logLn('ERROR');
333
        }*/
334 3
    }
335
336
    /**
337
     * Runs the actual conversion (after setup and checks)
338
     * Simply calls the doActualConvert() of the actual converter.
339
     * However, in the EncodingAutoTrait, this method is overridden to make two conversions
340
     * and select the smallest.
341
     *
342
     * @return void
343
     */
344 4
    protected function runActualConvert()
345
    {
346 4
        $this->doActualConvert();
347 3
    }
348
349
    /**
350
     * Convert an image to webp.
351
     *
352
     * @param   string  $source              path to source file
353
     * @param   string  $destination         path to destination
354
     * @param   array   $options (optional)  options for conversion
355
     * @param   BaseLogger $logger (optional)
356
     *
357
     * @throws  ConversionFailedException   in case conversion fails in an antipiciated way
358
     * @throws  \Exception   in case conversion fails in an unantipiciated way
359
     * @return  void
360
     */
361 13
    public static function convert($source, $destination, $options = [], $logger = null)
362
    {
363 13
        $c = self::createInstance($source, $destination, $options, $logger);
364 12
        $c->doConvert();
365
        //echo $instance->id;
366 3
    }
367
368
    /**
369
     * Get mime type for image (best guess).
370
     *
371
     * It falls back to using file extension. If that fails too, false is returned
372
     *
373
     * PS: Is it a security risk to fall back on file extension?
374
     * - By setting file extension to "jpg", one can lure our library into trying to convert a file, which isn't a jpg.
375
     * hmm, seems very unlikely, though not unthinkable that one of the converters could be exploited
376
     *
377
     * @return  string|false|null mimetype (if it is an image, and type could be determined / guessed),
378
     *    false (if it is not an image type that the server knowns about)
379
     *    or null (if nothing can be determined)
380
     */
381 31
    public function getMimeTypeOfSource()
382
    {
383 31
        if (!isset($this->sourceMimeType)) {
384 31
            $this->sourceMimeType = ImageMimeTypeGuesser::lenientGuess($this->source);
385
        }
386 31
        return $this->sourceMimeType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sourceMimeType also could return the type boolean which is incompatible with the documented return type false|null|string.
Loading history...
387
    }
388
}
389