Completed
Pull Request — master (#4)
by
unknown
11:23
created

FileBehavior::addClassAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
/**
4
 * @link https://github.com/rkit/filemanager-yii2
5
 * @copyright Copyright (c) 2015 Igor Romanov
6
 * @license [MIT](http://opensource.org/licenses/MIT)
7
 */
8
9
namespace rkit\filemanager\behaviors;
10
11
use rkit\filemanager\models\FileUploadSession;
12
use Yii;
13
use yii\base\Behavior;
14
use yii\base\Exception;
15
use yii\db\ActiveRecord;
16
use yii\helpers\ArrayHelper;
17
18
class FileBehavior extends Behavior
19
{
20
    /**
21
     * @var array
22
     */
23
    public $attributes = [];
24
25
    /**
26
     * @var ActiveQuery
27
     */
28
    private $relation;
29
30
    /**
31
     * @var FileBind
32
     */
33
    private $fileBind;
34
35
    /**
36
     * @var array
37
     */
38
    protected static $classPathMap = [];
39
40
    /**
41
     * @var string name of application component that represents `user`
42
     */
43
    public $userComponent = 'user';
44
45
    /**
46
     * @since 5.6.0
47
     * @var bool
48
     */
49
    protected $markedLinked = false;
50
51
    /**
52
     * @internal
53 31
     */
54
    public function init()
55 31
    {
56
        parent::init();
57 31
58
        $this->fileBind = new FileBind();
59 31
60 31
        Yii::$app->fileManager->registerTranslations();
61
    }
62
63
    /**
64
     * @inheritdoc
65
     * @internal
66 31
     */
67
    public function events()
68
    {
69 31
        return [
70 31
            ActiveRecord::EVENT_AFTER_INSERT  => 'afterSave',
71 31
            ActiveRecord::EVENT_AFTER_UPDATE  => 'afterSave',
72 31
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
73 31
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
74
            ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
75
        ];
76
    }
77
78
    /**
79
     * @internal
80
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
81
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
82 2
     */
83
    public function beforeSave($insert)
0 ignored issues
show
Unused Code introduced by
The parameter $insert is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
84 2
    {
85 2
        foreach ($this->attributes as $attribute => $options) {
86 2
            $oldValue = $this->owner->isNewRecord ? null : $this->owner->getOldAttribute($attribute);
87
            $isAttributeChanged = $oldValue === null ? true : $this->owner->isAttributeChanged($attribute);
88 2
89 2
            $this->attributes[$attribute]['isAttributeChanged'] = $isAttributeChanged;
90
            $this->attributes[$attribute]['oldValue'] = $oldValue;
91 2
        }
92
    }
93
94
    /**
95
     * @internal
96 2
     */
97
    public function afterSave()
98 2
    {
99 2
        foreach ($this->attributes as $attribute => $options)
100
        {
101 2
            $disableAutobind = $this->fileOption($attribute, 'disableAutobind');
102 2
            if ($disableAutobind) {
103 2
                continue;
104
            }
105
106 2
            $files = $this->owner->{$attribute};
107
108
            $isAttributeNotChanged = $options['isAttributeChanged'] === false || $files === null;
109
            if ($isAttributeNotChanged) {
110 2
                continue;
111 1
            }
112
113
            if (is_numeric($files)) {
114 2
                $files = [$files];
115 1
            }
116 1
117
            if (is_array($files)) {
118
                $files = array_filter($files);
119 1
            }
120 1
121 1
            if ($files === [] || $files === '') {
122
                $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
123
                continue;
124 1
            }
125
126 1
            $maxFiles = ArrayHelper::getValue($this->fileRules($attribute, true), 'maxFiles');
127
            if (is_array($files) && $maxFiles !== null) {
128 1
                $files = array_slice($files, 0, $maxFiles, true);
129
            }
130 1
131
            $files = $this->fileBind->bind($this->owner, $attribute, $files);
132
133 2
            $this->clearState($attribute, $files);
134
135
            if (is_array($files)) {
136
                $files = array_shift($files);
137
                $this->setValue($attribute, $files, $options['oldValue']);
138
            }
139
        }
140
    }
141
142
    /**
143
     * @internal
144
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
145
     */
146 26
    public function beforeDelete()
147
    {
148 26
        foreach ($this->attributes as $attribute => $options) {
149 26
            $disableAutobind = $this->fileOption($attribute, 'disableAutobind');
150
            if (!$disableAutobind) {
151
                $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
152
            }
153
        }
154 1
    }
155
156 1
    protected function getUser()
157 1
    {
158
        if (!$this->userComponent || !isset(Yii::$app->{$this->userComponent})) {
159
            return false;
160
        }
161
        return Yii::$app->{$this->userComponent};
162
    }
163
164
    public function clearState($attribute, $files)
165
    {
166
        if (!$this->getUser()) {
167
            return [];
168
        }
169
        if (!is_array($files)) {
170
            $files = [$files];
171
        }
172
        $query = [
173
            'created_user_id' => $this->getUser()->id,
174
            'target_model_class' => static::getClass(get_class($this->owner)),
175
            'target_model_id' => $this->owner->getPrimaryKey(),
176
            'target_model_attribute' => $attribute,
177
        ];
178 25
        if ($files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files 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...
179
            $fileIDs = ArrayHelper::getColumn($files, 'id');
180 25
            $query['file_id'] = $fileIDs;
181 25
        }
182
        FileUploadSession::deleteAll($query);
183
        $query['target_model_id'] = null;
184
        FileUploadSession::deleteAll($query);  // for cases of uploads when original model was a new record at the moment of uploads
185
        return;
186
    }
187
188
    private function setState($attribute, $file)
189
    {
190
        $rec = new FileUploadSession();
191
        $rec->created_user_id = $this->getUser()->id;
192
        $rec->file_id = $file->getPrimaryKey();
193
        $rec->target_model_attribute = $attribute; // TODO: write model/object id?
194
        $rec->target_model_id = (!$this->owner->isNewRecord ? $this->owner->getPrimaryKey() : null);
195
        $rec->target_model_class = static::getClass(get_class($this->owner));
196
        $rec->save(false);
197
    }
198
199
    /**
200
     * for models with single upload only
201
     * @param $attribute
202
     * @param $file
203
     * @param $defaultValue
204
     */
205
    private function setValue($attribute, $file, $defaultValue)
206
    {
207
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
208
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
209
210
        if ($saveFilePath || $saveFileId) {
211
            if (!$file) {
212
                $value = $defaultValue;
213
            } elseif ($saveFilePath) {
214
                $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
215
                $value = Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $handlerTemplatePath($file);
216
            } elseif ($saveFileId) {
217
                $value = $file->getPrimaryKey();
218
            }
219
            $this->owner->{$attribute} = $value;
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
220
            $this->owner->updateAttributes([$attribute => $value]);
221
        }
222
    }
223
224
    /**
225
     * Generate a thumb
226
     *
227
     * @param string $attribute The attribute name
228
     * @param string $preset The preset name
229
     * @param string $path The file path
230
     * @return string The thumb path
231
     */
232
    private function generateThumb($attribute, $preset, $path)
233
    {
234
        $thumbPath = pathinfo($path, PATHINFO_FILENAME);
235
        $thumbPath = str_replace($thumbPath, $preset . '_' . $thumbPath, $path);
236
        $realPath = $this->fileStorage($attribute)->path;
237
238
        if (!file_exists($realPath . $thumbPath) && file_exists($realPath . $path)) {
239
            $handlerPreset = $this->fileOption($attribute, 'preset.'.$preset);
240
            $handlerPreset($realPath, $path, $thumbPath);
241
        }
242
243
        return $thumbPath;
244
    }
245
246
    /**
247
     * Generate file path by template
248
     *
249
     * @param string $attribute The attribute name
250
     * @param ActiveRecord $file The file model
251
     * @return string The file path
252
     */
253
    private function templatePath($attribute, $file = null)
254
    {
255
        $value = $this->owner->{$attribute};
256
257
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
258
        $isFilledPath = $saveFilePath && !empty($value);
259
260
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
261
        $isFilledId = $saveFileId && is_numeric($value) && $value;
262
263
        if (($isFilledPath || $isFilledId) && $file === null) {
264
            $file = $this->file($attribute);
265
        }
266
267
        if ($file !== null) {
268
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
269
            return $handlerTemplatePath($file);
270 2
        }
271
        return $value;
272 2
    }
273 2
274
    /**
275 2
     * Get relation
276
     *
277
     * @param string $attribute The attribute name
278
     * @return \ActiveQuery
279
     */
280
    public function fileRelation($attribute)
281
    {
282
        if ($this->relation === null) {
283
            $this->relation = $this->owner->getRelation($this->fileOption($attribute, 'relation'));
284
        }
285
        return $this->relation;
286 30
    }
287
288 30
    /**
289
     * Get file option
290
     *
291
     * @param string $attribute The attribute name
292
     * @param string $option Option name
293
     * @param mixed $defaultValue Default value
294
     * @return mixed
295
     */
296
    public function fileOption($attribute, $option, $defaultValue = null)
297 26
    {
298
        return ArrayHelper::getValue($this->attributes[$attribute], $option, $defaultValue);
299 26
    }
300
301
    /**
302
     * Get file storage
303
     *
304
     * @param string $attribute The attribute name
305
     * @return \Flysystem
306
     */
307
    public function fileStorage($attribute)
308
    {
309
        return Yii::$app->get($this->fileOption($attribute, 'storage'));
310
    }
311
312
    /**
313
     * Get file path
314
     *
315
     * @param string $attribute The attribute name
316
     * @param ActiveRecord $file Use this file model
317
     * @return string The file path
318
     */
319
    public function filePath($attribute, $file = null)
320
    {
321
        $path = $this->templatePath($attribute, $file);
322
        return $this->fileStorage($attribute)->path . $path;
323
    }
324
325
    /**
326
     * Get file url
327
     *
328
     * @param string $attribute The attribute name
329
     * @param ActiveRecord $file Use this file model
330
     * @return string The file url
331
     */
332
    public function fileUrl($attribute, $file = null)
333
    {
334
        $path = $this->templatePath($attribute, $file);
335
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $path;
336
    }
337
338
    /**
339
     * Get extra fields of file
340
     *
341
     * @param string $attribute The attribute name
342
     * @return array
343
     */
344
    public function fileExtraFields($attribute)
345
    {
346
        if ($this->fileOption($attribute, 'disableAutobind')) {
347
            return [];
348
        }
349 2
        $fields = $this->fileBind->relations($this->owner, $attribute);
350
        if (!$this->fileOption($attribute, 'multiple')) {
351 2
            return array_shift($fields);
352
        }
353
        return $fields;
354
    }
355
356
    /**
357
     * Get files
358
     *
359
     * @param string $attribute The attribute name
360 1
     * @return \ActiveRecord[] The file models
361
     */
362 1
    public function files($attribute)
363
    {
364
        if ($this->fileOption($attribute, 'disableAutobind')) {
365
            throw new Exception('Accessing `files()` is not allowed when auto-bind is disabled, see `FileBehavior::$disableAutobind`');
366
        }
367
        return $this->fileBind->files($this->owner, $attribute);
368
    }
369
370
    /**
371
     * Get the file
372 29
     *
373
     * @param string $attribute The attribute name
374 29
     * @return \ActiveRecord The file model
375 29
     */
376 28
    public function file($attribute)
377 28
    {
378
        if ($this->fileOption($attribute, 'disableAutobind')) {
379 29
            throw new Exception('Accessing `file()` is not allowed when auto-bind is disabled, see `FileBehavior::$disableAutobind`');
380
        }
381
        return $this->fileBind->file($this->owner, $attribute);
382
    }
383
384
    /**
385
     * Get rules
386
     *
387
     * @param string $attribute The attribute name
388
     * @param bool $onlyCoreValidators Only core validators
389
     * @return array
390
     */
391
    public function fileRules($attribute, $onlyCoreValidators = false)
392
    {
393
        $rules = $this->fileOption($attribute, 'rules', []);
394
        if ($onlyCoreValidators && isset($rules['imageSize'])) {
395
            $rules = array_merge($rules, $rules['imageSize']);
396
            unset($rules['imageSize']);
397
        }
398
        return $rules;
399
    }
400
401
    /**
402
     * Get file state
403
     *
404
     * @param string $attribute The attribute name
405
     * @return array
406
     */
407
    public function fileState($attribute)
408
    {
409
        if (!$this->getUser()) {
410
            return [];
411
        }
412
        $query = FileUploadSession::find()->where([
413
            'created_user_id' => $this->getUser()->id,
414
            'target_model_class' => static::getClass(get_class($this->owner)),
415
            'target_model_attribute' => $attribute,
416
        ]);
417
        $query->andWhere(['or',
418
            ['target_model_id' => $this->owner->getPrimaryKey()],
419
            ['target_model_id' => null] // for cases of uploads when original model was a new record at the moment of uploads
420
        ]);
421
        $data = $query->all();
422
        if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
423
            return ArrayHelper::getColumn($data, ['file_id']);
0 ignored issues
show
Documentation introduced by
array('file_id') is of type array<integer,string,{"0":"string"}>, but the function expects a string|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
424
        } else {
425
            return [];
426
        }
427
    }
428
429
    /**
430
     * Get the presets of the file for apply after upload
431
     *
432
     * @param string $attribute The attribute name
433
     * @return array
434
     */
435
    public function filePresetAfterUpload($attribute)
436
    {
437
        $preset = $this->fileOption($attribute, 'applyPresetAfterUpload', []);
438
        if (is_string($preset) && $preset === '*') {
439
            return array_keys($this->fileOption($attribute, 'preset', []));
440
        }
441
        return $preset;
442
    }
443
444
    /**
445
     * Create a thumb and return url
446
     *
447
     * @param string $attribute The attribute name
448
     * @param string $preset The preset name
449
     * @param ActiveRecord $file Use this file model
450
     * @return string The file url
451
     */
452
    public function thumbUrl($attribute, $preset, $file = null)
453
    {
454
        $path = $this->templatePath($attribute, $file);
455
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
456
457
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $thumbPath;
458
    }
459
460
    /**
461
     * Create a thumb and return full path
462
     *
463
     * @param string $attribute The attribute name
464
     * @param string $preset The preset name
465 25
     * @param ActiveRecord $file Use this file model
466
     * @return string The file path
467 25
     */
468 25
    public function thumbPath($attribute, $preset, $file = null)
469 25
    {
470 25
        $path = $this->templatePath($attribute, $file);
471 25
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
472 25
473 25
        return $this->fileStorage($attribute)->path . $thumbPath;
474 25
    }
475 25
476
    /**
477
     * Create a file
478
     *
479
     * @param string $attribute The attribute name
480
     * @param string $path The file path
481
     * @param string $name The file name
482
     * @return \ActiveRecord The file model
483
     */
484
    public function createFile($attribute, $path, $name)
485
    {
486
        $handlerCreateFile = $this->fileOption($attribute, 'createFile');
487
        $file = $handlerCreateFile($path, $name);
488
        if ($file) {
489
            $storage = $this->fileStorage($attribute);
490
            $contents = file_get_contents($path);
491
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
492
            if ($storage->write($handlerTemplatePath($file), $contents, [
493
                // set correct mime type:
494
                'mimetype' => yii\helpers\FileHelper::getMimeTypeByExtension($name),
495
            ])) {
496
                $disableAutobind = $this->fileOption($attribute, 'disableAutobind');
497
                if (!$this->markedLinked && !$disableAutobind) {
498
                    $this->setState($attribute, $file);
499
                }
500
                $this->owner->{$attribute} = $file->id;
501
                return $file;
502
            }
503
        } // @codeCoverageIgnore
504
        return false; // @codeCoverageIgnore
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by rkit\filemanager\behavio...ileBehavior::createFile of type ActiveRecord.

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...
505
    }
506
507
    /**
508
     * Create a file from remote URL
509
     *
510
     * @author Sergii Gamaiunov <[email protected]>
511
     *
512
     * @param string $attribute The attribute name
513
     * @param \igogo5yo\uploadfromurl\UploadFromUrl $remoteFile
514
     * @param string $name The file name
515
     * @return \ActiveRecord The file model
516
     */
517
    public function createRemoteFile($attribute, $remoteFile, $name)
518
    {
519
        $url = $remoteFile->url;
520
        $handlerCreateFile = $this->fileOption($attribute, 'createRemoteFile');
521
        $file = $handlerCreateFile($remoteFile, $name);
522
        if ($file) {
523
            $storage = $this->fileStorage($attribute);
524
            $stream = fopen($url, 'r');
525
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
526
            if ($storage->putStream($handlerTemplatePath($file), $stream)) {
527
                if (is_resource($stream)) { // some adapters close resources on their own
528
                    fclose($stream);
529
                }
530
                if ($this->getUser()) {
531
                    if (!$this->markedLinked) {
532
                        $this->setState($attribute, $file);
533
                    }
534
                }
535
                $this->owner->{$attribute} = $file->id;
536
                return $file;
537
            }
538
        } // @codeCoverageIgnore
539
        return false; // @codeCoverageIgnore
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by rkit\filemanager\behavio...avior::createRemoteFile of type ActiveRecord.

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...
540
    }
541
542
    /**
543
     * Add class alias to be able to upload files for different versions of a model to a single API endpoint
544
     *
545
     * Example:
546
     * ```
547
     * class OldCar extends Car
548
     * {
549
     *      public function init()
550
     *      {
551
     *          parent::init();
552
     *          $this->car_type = 'old;
553
     *          FileBehavior::addClassAlias(get_class($this), Car::className());
554
     *      }
555
     *
556
     *      public function formName() {
557
     *          return 'Car';
558
     *      }
559
     * }
560
     * ```
561
     * @param $source
562
     * @param $mapTo
563
     */
564
    public static function addClassAlias($source, $mapTo) {
565
        static::$classPathMap[$source] = $mapTo;
566
    }
567
568
    protected static function getClass($source) {
569
        return isset(static::$classPathMap[$source])
570
            ? static::$classPathMap[$source]
571
            : $source;
572
    }
573
574
    /**
575
     * Mark current upload session as already linked (e.g. file is linked during `createFile`) to avoid duplicate links
576
     * @return $this
577
     * @since 5.6.0
578
     */
579
    public function markLinked() {
580
        $this->markedLinked = true;
581
        return $this;
582
    }
583
}
584