Passed
Push — master ( a9f547...49cca7 )
by Raffael
04:18
created

File   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 774
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 9.39%

Importance

Changes 0
Metric Value
wmc 77
lcom 1
dl 0
loc 774
ccs 31
cts 330
cp 0.0939
rs 1.7391
c 0
b 0
f 0
cbo 15

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A get() 0 15 3
B copyTo() 0 32 5
A getHistory() 0 4 1
C restore() 0 84 9
B delete() 0 43 5
A isTemporaryFile() 0 10 3
B deleteVersion() 0 32 5
A cleanHistory() 0 8 2
B getAttributes() 0 26 1
A getExtension() 0 9 2
A getSize() 0 4 1
A getETag() 0 4 1
A getHash() 0 4 1
A getVersion() 0 4 1
B put() 0 58 7
A _forceDelete() 0 22 2
A increaseVersion() 0 16 2
A guidv4() 0 9 1
B validatePutRequest() 0 27 6
B verifyFile() 0 28 4
B createTemporaryFile() 0 36 6
B addVersion() 0 48 5
B postPutFile() 0 30 2

How to fix   Complexity   

Complex Class

Complex classes like File often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use File, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception as AclException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Filesystem\Storage;
19
use Balloon\Filesystem\Storage\Exception as StorageException;
20
use Balloon\Hook;
21
use Balloon\Mime;
22
use MongoDB\BSON\UTCDateTime;
23
use Psr\Log\LoggerInterface;
24
use Sabre\DAV\IFile;
25
26
class File extends AbstractNode implements IFile
27
{
28
    /**
29
     * History types.
30
     */
31
    const HISTORY_CREATE = 0;
32
    const HISTORY_EDIT = 1;
33
    const HISTORY_RESTORE = 2;
34
    const HISTORY_DELETE = 3;
35
    const HISTORY_UNDELETE = 4;
36
37
    /**
38
     * Empty content hash (NULL).
39
     */
40
    const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e';
41
42
    /**
43
     * Temporary file patterns.
44
     *
45
     * @param array
46
     **/
47
    protected $temp_files = [
48
        '/^\._(.*)$/',     // OS/X resource forks
49
        '/^.DS_Store$/',   // OS/X custom folder settings
50
        '/^desktop.ini$/', // Windows custom folder settings
51
        '/^Thumbs.db$/',   // Windows thumbnail cache
52
        '/^.(.*).swpx$/',  // ViM temporary files
53
        '/^.(.*).swx$/',   // ViM temporary files
54
        '/^.(.*).swp$/',   // ViM temporary files
55
        '/^\.dat(.*)$/',   // Smultron seems to create these
56
        '/^~lock.(.*)#$/', // Windows 7 lockfiles
57
    ];
58
59
    /**
60
     * MD5 Hash of the content.
61
     *
62
     * @var string
63
     */
64
    protected $hash;
65
66
    /**
67
     * File version.
68
     *
69
     * @var int
70
     */
71
    protected $version = 0;
72
73
    /**
74
     * File size.
75
     *
76
     * @var int
77
     */
78
    protected $size = 0;
79
80
    /**
81
     * History.
82
     *
83
     * @var array
84
     */
85
    protected $history = [];
86
87
    /**
88
     * Storage.
89
     *
90
     * @var Storage
91
     */
92
    protected $_storage;
93
94
    /**
95
     * Storage attributes.
96
     *
97
     * @var mixed
98
     */
99
    protected $storage;
100
101
    /**
102
     * Initialize file node.
103
     */
104 10
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Storage $storage)
105
    {
106 10
        $this->_fs = $fs;
107 10
        $this->_server = $fs->getServer();
108 10
        $this->_db = $fs->getDatabase();
109 10
        $this->_user = $fs->getUser();
110 10
        $this->_logger = $logger;
111 10
        $this->_hook = $hook;
112 10
        $this->_storage = $storage;
113 10
        $this->_acl = $acl;
114
115 10
        foreach ($attributes as $attr => $value) {
116 10
            $this->{$attr} = $value;
117
        }
118
119 10
        $this->raw_attributes = $attributes;
120 10
    }
121
122
    /**
123
     * Read content and return ressource.
124
     *
125
     * @return resource
126
     */
127
    public function get()
128
    {
129
        try {
130
            if (null === $this->storage) {
131
                return null;
132
            }
133
134
            return $this->_storage->getFile($this, $this->storage);
135
        } catch (\Exception $e) {
136
            throw new Exception\NotFound(
137
                'content not found',
138
                Exception\NotFound::CONTENTS_NOT_FOUND
139
            );
140
        }
141
    }
142
143
    /**
144
     * Copy node.
145
     *
146
     * @param string $recursion
147
     */
148
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
149
    {
150
        $this->_hook->run(
151
            'preCopyFile',
152
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
153
        );
154
155
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
156
            $name = $this->getDuplicateName();
157
        } else {
158
            $name = $this->name;
159
        }
160
161
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
162
            $result = $parent->getChild($this->name);
163
            $result->put($this->get());
164
        } else {
165
            $result = $parent->addFile($name, $this->get(), [
166
                'created' => $this->created,
167
                'changed' => $this->changed,
168
                'deleted' => $this->deleted,
169
                'meta' => $this->meta,
170
            ], NodeInterface::CONFLICT_NOACTION, true);
171
        }
172
173
        $this->_hook->run(
174
            'postCopyFile',
175
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
176
        );
177
178
        return $result;
179
    }
180
181
    /**
182
     * Get history.
183
     */
184
    public function getHistory(): array
185
    {
186
        return $this->history;
187
    }
188
189
    /**
190
     * Restore content to some older version.
191
     */
192
    public function restore(int $version): bool
193
    {
194
        if (!$this->_acl->isAllowed($this, 'w')) {
195
            throw new AclException\Forbidden(
196
                'not allowed to restore node '.$this->name,
197
                AclException\Forbidden::NOT_ALLOWED_TO_RESTORE
198
            );
199
        }
200
201
        $this->_hook->run('preRestoreFile', [$this, &$version]);
202
203
        if ($this->readonly) {
204
            throw new Exception\Conflict(
205
                'node is marked as readonly, it is not possible to change any content',
206
                Exception\Conflict::READONLY
207
            );
208
        }
209
210
        if ($this->version === $version) {
211
            throw new Exception('file is already version '.$version);
212
        }
213
214
        $current = $this->version;
0 ignored issues
show
Unused Code introduced by
$current is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
215
        $new = $this->increaseVersion();
216
217
        $v = array_search($version, array_column($this->history, 'version'), true);
218
        if (false === $v) {
219
            throw new Exception('failed restore file to version '.$version.', version was not found');
220
        }
221
222
        $file = $this->history[$v]['storage'];
223
224
        $this->history[] = [
225
            'version' => $new,
226
            'changed' => $this->changed,
227
            'user' => $this->owner,
228
            'type' => self::HISTORY_RESTORE,
229
            'hash' => $this->history[$v]['hash'],
230
            'origin' => $this->history[$v]['version'],
231
            'storage' => $this->history[$v]['storage'],
232
            'storage_adapter' => $this->history[$v]['storage_adapter'],
233
            'size' => $this->history[$v]['size'],
234
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null,
235
        ];
236
237
        try {
238
            $this->deleted = false;
239
            $this->version = $new;
240
            $this->storage = $this->history[$v]['storage'];
241
            $this->storage_adapter = $this->history[$v]['storage_adapter'];
242
243
            $this->hash = null === $file ? self::EMPTY_CONTENT : $this->history[$v]['hash'];
244
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null;
245
            $this->size = $this->history[$v]['size'];
246
            $this->changed = $this->history[$v]['changed'];
247
248
            $this->save([
249
                'deleted',
250
                'version',
251
                'storage',
252
                'storage_adapter',
253
                'hash',
254
                'mime',
255
                'size',
256
                'history',
257
                'changed',
258
            ]);
259
260
            $this->_hook->run('postRestoreFile', [$this, &$version]);
261
262
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
263
                'category' => get_class($this),
264
            ]);
265
        } catch (\Exception $e) {
266
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
267
                'category' => get_class($this),
268
                'exception' => $e,
269
            ]);
270
271
            throw $e;
272
        }
273
274
        return true;
275
    }
276
277
    /**
278
     * Delete node.
279
     *
280
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
281
     * delete finally
282
     *
283
     * @param string $recursion
284
     */
285
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
286
    {
287
        if (!$this->_acl->isAllowed($this, 'w')) {
288
            throw new AclException\Forbidden(
289
                'not allowed to delete node '.$this->name,
290
                AclException\Forbidden::NOT_ALLOWED_TO_DELETE
291
            );
292
        }
293
294
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
295
296
        if (true === $force || $this->isTemporaryFile()) {
297
            $result = $this->_forceDelete();
298
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
299
300
            return $result;
301
        }
302
303
        $ts = new UTCDateTime();
304
        $this->deleted = $ts;
305
        $this->increaseVersion();
306
307
        $this->history[] = [
308
            'version' => $this->version,
309
            'changed' => $ts,
310
            'user' => ($this->_user === null) ? null : $this->_user->getId(),
311
            'type' => self::HISTORY_DELETE,
312
            'storage' => $this->storage,
313
            'storage_adapter' => $this->storage_adapter,
314
            'size' => $this->size,
315
            'hash' => $this->hash,
316
        ];
317
318
        $result = $this->save([
319
            'version',
320
            'deleted',
321
            'history',
322
        ], [], $recursion, $recursion_first);
323
324
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
325
326
        return $result;
327
    }
328
329
    /**
330
     * Check if file is temporary.
331
     *
332
     **/
333 2
    public function isTemporaryFile(): bool
334
    {
335 2
        foreach ($this->temp_files as $pattern) {
336 2
            if (preg_match($pattern, $this->name)) {
337 2
                return true;
338
            }
339
        }
340
341 1
        return false;
342
    }
343
344
    /**
345
     * Delete version.
346
     */
347
    public function deleteVersion(int $version): bool
348
    {
349
        $key = array_search($version, array_column($this->history, 'version'), true);
350
351
        if (false === $key) {
352
            throw new Exception('version '.$version.' does not exists');
353
        }
354
355
        $blobs = array_column($this->history, 'storage');
356
357
        try {
358
            //do not remove blob if there are other versions linked against it
359
            if ($this->history[$key]['storage'] !== null && count(array_keys($blobs, $this->history[$key]['storage'])) === 1) {
360
                $this->_storage->deleteFile($this, $this->history[$key]['storage']);
361
            }
362
363
            array_splice($this->history, $key, 1);
364
365
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
366
                'category' => get_class($this),
367
            ]);
368
369
            return $this->save('history');
370
        } catch (StorageException\NotFound $e) {
371
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
372
                'category' => get_class($this),
373
                'exception' => $e,
374
            ]);
375
376
            return false;
377
        }
378
    }
379
380
    /**
381
     * Cleanup history.
382
     */
383
    public function cleanHistory(): bool
384
    {
385
        foreach ($this->history as $node) {
386
            $this->deleteVersion($node['version']);
387
        }
388
389
        return true;
390
    }
391
392
    /**
393
     * Get Attributes.
394
     */
395
    public function getAttributes(): array
396
    {
397
        return [
398
            '_id' => $this->_id,
399
            'name' => $this->name,
400
            'hash' => $this->hash,
401
            'directory' => false,
402
            'size' => $this->size,
403
            'version' => $this->version,
404
            'parent' => $this->parent,
405
            'acl' => $this->acl,
406
            'app' => $this->app,
407
            'meta' => $this->meta,
408
            'mime' => $this->mime,
409
            'owner' => $this->owner,
410
            'history' => $this->history,
411
            'shared' => $this->shared,
412
            'deleted' => $this->deleted,
413
            'changed' => $this->changed,
414
            'created' => $this->created,
415
            'destroy' => $this->destroy,
416
            'readonly' => $this->readonly,
417
            'storage_adapter' => $this->storage_adapter,
418
            'storage' => $this->storage,
419
        ];
420
    }
421
422
    /**
423
     * Get filename extension.
424
     */
425 2
    public function getExtension(): string
426
    {
427 2
        $ext = strrchr($this->name, '.');
428 2
        if (false === $ext) {
429 1
            throw new Exception('file does not have an extension');
430
        }
431
432 1
        return substr($ext, 1);
433
    }
434
435
    /**
436
     * Get file size.
437
     */
438 1
    public function getSize(): int
439
    {
440 1
        return $this->size;
441
    }
442
443
    /**
444
     * Get md5 sum of the file content,
445
     * actually the hash value comes from the database.
446
     */
447 1
    public function getETag(): string
448
    {
449 1
        return "'".$this->hash."'";
450
    }
451
452
    /**
453
     * Get hash.
454
     *
455
     * @return string
456
     */
457 1
    public function getHash(): ?string
458
    {
459 1
        return $this->hash;
460
    }
461
462
    /**
463
     * Get version.
464
     */
465 1
    public function getVersion(): int
466
    {
467 1
        return $this->version;
468
    }
469
470
    /**
471
     * Change content.
472
     *
473
     * @param resource|string $file
474
     */
475
    public function put($file, bool $new = false, array $attributes = []): int
476
    {
477
        $this->_logger->debug('add contents for file ['.$this->_id.']', [
478
            'category' => get_class($this),
479
        ]);
480
481
        $this->validatePutRequest($file, $new, $attributes);
482
        $file = $this->createTemporaryFile($file, $stream);
483
        $new_hash = $this->verifyFile($file, $new);
484
485
        if ($this->hash === $new_hash) {
486
            $this->_logger->info('stop PUT execution, content checksums are equal for file ['.$this->_id.']', [
487
                'category' => get_class($this),
488
            ]);
489
490
            //Remove tmp file
491
            if (null !== $file) {
492
                unlink($file);
493
                fclose($stream);
494
            }
495
496
            return $this->version;
497
        }
498
499
        $this->hash = $new_hash;
500
501
        //Write new content
502
        if ($this->size > 0) {
503
            $this->storage = $this->_storage->storeFile($this, $stream, $this->storage_adapter);
504
        } else {
505
            $this->storage = null;
506
        }
507
508
        //Update current version
509
        $this->increaseVersion();
510
511
        //Get meta attributes
512
        if (isset($attributes['mime'])) {
513
            $this->mime = $attributes['mime'];
514
        } elseif (null !== $file) {
515
            $this->mime = (new Mime())->getMime($file, $this->name);
516
        }
517
518
        //Remove tmp file
519
        if (null !== $file) {
520
            unlink($file);
521
            fclose($stream);
522
        }
523
524
        $this->_logger->debug('set mime ['.$this->mime.'] for content, file=['.$this->_id.']', [
525
            'category' => get_class($this),
526
        ]);
527
528
        $this->addVersion($attributes)
529
             ->postPutFile($file, $new, $attributes);
530
531
        return $this->version;
532
    }
533
534
    /**
535
     * Completly remove file.
536
     */
537
    protected function _forceDelete(): bool
538
    {
539
        try {
540
            $this->cleanHistory();
541
            $this->_db->storage->deleteOne([
542
                '_id' => $this->_id,
543
            ]);
544
545
            $this->_logger->info('removed file node ['.$this->_id.']', [
546
                'category' => get_class($this),
547
            ]);
548
        } catch (\Exception $e) {
549
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
550
                'category' => get_class($this),
551
                'exception' => $e,
552
            ]);
553
554
            throw $e;
555
        }
556
557
        return true;
558
    }
559
560
    /**
561
     * Increase version.
562
     */
563
    protected function increaseVersion(): int
564
    {
565
        $max = $this->_fs->getServer()->getMaxFileVersion();
566
        if (count($this->history) >= $max) {
567
            $del = key($this->history);
568
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$del.'] from file ['.$this->_id.']', [
569
                'category' => get_class($this),
570
            ]);
571
572
            $this->deleteVersion($this->history[$del]['version']);
573
        }
574
575
        ++$this->version;
576
577
        return $this->version;
578
    }
579
580
    /**
581
     * Create uuidv4.
582
     */
583
    protected function guidv4(string $data): string
584
    {
585
        assert(16 === strlen($data));
586
587
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
588
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
589
590
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
591
    }
592
593
    /**
594
     * Change content.
595
     *
596
     * @param resource|string $file
597
     */
598
    protected function validatePutRequest($file, bool $new = false, array $attributes = []): bool
599
    {
600
        if (!$this->_acl->isAllowed($this, 'w')) {
601
            throw new AclException\Forbidden(
602
                'not allowed to modify node',
603
                AclException\Forbidden::NOT_ALLOWED_TO_MODIFY
604
            );
605
        }
606
607
        $this->_hook->run('prePutFile', [$this, &$file, &$new, &$attributes]);
608
609
        if ($this->readonly) {
610
            throw new Exception\Conflict(
611
                'node is marked as readonly, it is not possible to change any content',
612
                Exception\Conflict::READONLY
613
            );
614
        }
615
616
        if ($this->isShareMember() && false === $new && 'w' === $this->_acl->getAclPrivilege($this->getShareNode())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getShareNode() can be null; however, getAclPrivilege() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
617
            throw new AclException\Forbidden(
618
                'not allowed to overwrite node',
619
                AclException\Forbidden::NOT_ALLOWED_TO_OVERWRITE
620
            );
621
        }
622
623
        return true;
624
    }
625
626
    /**
627
     * Verify content to be added.
628
     *
629
     * @param string $path
630
     *
631
     * @return bool
632
     */
633
    protected function verifyFile(?string $path, bool $new = false): string
634
    {
635
        if (null === $path) {
636
            $this->size = 0;
637
            $new_hash = self::EMPTY_CONTENT;
638
        } else {
639
            $size = filesize($path);
640
            $this->size = $size;
641
            $new_hash = md5_file($path);
642
643
            if (!$this->_user->checkQuota($size)) {
644
                $this->_logger->warning('could not execute PUT, user quota is full', [
645
                    'category' => get_class($this),
646
                ]);
647
648
                if (true === $new) {
649
                    $this->_forceDelete();
650
                }
651
652
                throw new Exception\InsufficientStorage(
653
                    'user quota is full',
654
                    Exception\InsufficientStorage::USER_QUOTA_FULL
655
                );
656
            }
657
        }
658
659
        return $new_hash;
660
    }
661
662
    /**
663
     * Create temporary file.
664
     *
665
     * @param resource|string $file
666
     * @param resource        $stream
667
     *
668
     * @return string
669
     */
670
    protected function createTemporaryFile($file, &$stream): ?string
671
    {
672
        if (is_string($file)) {
673
            if (!is_readable($file)) {
674
                throw new Exception('file does not exists or is not readable');
675
            }
676
677
            $stream = fopen($file, 'r');
678
        } elseif (is_resource($file)) {
679
            $tmp = $this->_fs->getServer()->getTempDir().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.$this->_user->getId();
680
            if (!file_exists($tmp)) {
681
                mkdir($tmp, 0700, true);
682
            }
683
684
            $tmp_file = $tmp.DIRECTORY_SEPARATOR.$this->guidv4(openssl_random_pseudo_bytes(16));
685
            $stream = fopen($tmp_file, 'w+');
686
            $size = stream_copy_to_stream($file, $stream, ((int) $this->_fs->getServer()->getMaxFileSize() + 1));
687
            rewind($stream);
688
            fclose($file);
689
690
            if ($size > (int) $this->_fs->getServer()->getMaxFileSize()) {
691
                unlink($tmp_file);
692
693
                throw new Exception\InsufficientStorage(
694
                    'file size exceeded limit',
695
                    Exception\InsufficientStorage::FILE_SIZE_LIMIT
696
                );
697
            }
698
699
            $file = $tmp_file;
700
        } else {
701
            $file = null;
702
        }
703
704
        return $file;
705
    }
706
707
    /**
708
     * Add new version.
709
     *
710
     *
711
     * @return File
712
     */
713
    protected function addVersion(array $attributes = []): self
714
    {
715
        if (1 !== $this->version) {
716
            if (isset($attributes['changed'])) {
717
                if (!($attributes['changed'] instanceof UTCDateTime)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
718
                    throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
719
                }
720
721
                $this->changed = $attributes['changed'];
722
            } else {
723
                $this->changed = new UTCDateTime();
724
            }
725
726
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
727
                'category' => get_class($this),
728
            ]);
729
730
            $this->history[] = [
731
                'version' => $this->version,
732
                'changed' => $this->changed,
733
                'user' => $this->_user->getId(),
734
                'type' => self::HISTORY_EDIT,
735
                'storage' => $this->storage,
736
                'storage_adapter' => $this->storage_adapter,
737
                'size' => $this->size,
738
                'mime' => $this->mime,
739
                'hash' => $this->hash,
740
            ];
741
        } else {
742
            $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
743
                'category' => get_class($this),
744
            ]);
745
746
            $this->history[0] = [
747
                'version' => 1,
748
                'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
749
                'user' => $this->owner,
750
                'type' => self::HISTORY_CREATE,
751
                'storage' => $this->storage,
752
                'storage_adapter' => $this->storage_adapter,
753
                'size' => $this->size,
754
                'mime' => $this->mime,
755
                'hash' => $this->hash,
756
            ];
757
        }
758
759
        return $this;
760
    }
761
762
    /**
763
     * Finalize put request.
764
     *
765
     * @param resource|string $file
766
     *
767
     * @return File
768
     */
769
    protected function postPutFile($file, bool $new, array $attributes): self
770
    {
771
        try {
772
            $this->save([
773
                'size',
774
                'changed',
775
                'mime',
776
                'hash',
777
                'version',
778
                'history',
779
                'storage',
780
                'storage_adapter',
781
            ]);
782
783
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
784
                'category' => get_class($this),
785
            ]);
786
787
            $this->_hook->run('postPutFile', [$this, $file, $new, $attributes]);
788
789
            return $this;
790
        } catch (\Exception $e) {
791
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
792
                'category' => get_class($this),
793
                'exception' => $e,
794
            ]);
795
796
            throw $e;
797
        }
798
    }
799
}
800