Issues (51)

src/Model/Traits/AbstractFile.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Model\Traits;
6
7
use Doctrine\ORM\Mapping as ORM;
8
use Exception;
9
use GraphQL\Doctrine\Attribute as API;
10
use Psr\Http\Message\UploadedFileInterface;
11
12
/**
13
 * Wrapping class for an uploaded file.
14
 */
15
trait AbstractFile
16
{
17
    /**
18
     * Get base path where the files are stored in the server.
19
     */
20
    abstract protected function getBasePath(): string;
21
22
    /**
23
     * Get list of accepted MIME types.
24
     *
25
     * @return string[]
26
     */
27
    abstract protected function getAcceptedMimeTypes(): array;
28
29
    #[ORM\Column(type: 'string', length: 190, options: ['default' => ''])]
30
    #[API\Exclude]
31
    private string $filename = '';
32
33
    #[ORM\Column(type: 'string', length: 255, options: ['default' => ''])]
34
    private string $mime = '';
35
36
    /**
37
     * Set the file.
38
     */
39 8
    public function setFile(UploadedFileInterface $file): void
40
    {
41 8
        $this->generateUniqueFilename($file->getClientFilename() ?? '');
42
43 8
        $path = $this->getPath();
44 8
        if (file_exists($path)) {
45
            throw new Exception('A file already exist with the same name: ' . $this->getFilename());
46
        }
47 8
        $file->moveTo($path);
48
49 8
        $this->validateMimeType();
50
    }
51
52
    /**
53
     * Set filename (without path).
54
     */
55 10
    #[API\Exclude]
56
    public function setFilename(string $filename): void
57
    {
58 10
        $this->filename = $filename;
59
    }
60
61
    /**
62
     * Get filename (without path).
63
     */
64 10
    #[API\Exclude]
65
    public function getFilename(): string
66
    {
67 10
        return $this->filename;
68
    }
69
70
    /**
71
     * Get mime.
72
     */
73 8
    public function getMime(): string
74
    {
75 8
        return $this->mime;
76
    }
77
78
    /**
79
     * Get absolute path to file on disk.
80
     */
81 2
    #[API\Exclude]
82
    public function getPath(): string
83
    {
84 2
        return realpath('.') . '/' . $this->getBasePath() . $this->getFilename();
85
    }
86
87
    /**
88
     * Automatically called by Doctrine before the object is deleted.
89
     *
90
     * Without this, the record would not exist anymore in DB when we try to
91
     * get the file path in `deleteFile()`. And then we could not delete the
92
     * file on disk after the record deletion.
93
     */
94
    #[ORM\PreRemove]
95
    public function triggerLazyLoading(): void
96
    {
97
        $this->getPath();
98
    }
99
100
    /**
101
     * Automatically called by Doctrine when the object is deleted
102
     * Is called after database update because we can have issues on remove operation (like integrity test)
103
     * and it's preferable to keep a related file on drive before removing it definitely.
104
     */
105
    #[ORM\PostRemove]
106
    public function deleteFile(): void
107
    {
108
        $path = $this->getPath();
109
        if (file_exists($path) && is_file($path) && !str_contains($this->getFilename(), 'dw4jV3zYSPsqE2CB8BcP8ABD0.')) {
110
            unlink($path);
111
        }
112
    }
113
114
    /**
115
     * Generate unique filename while trying to preserve original extension.
116
     */
117 8
    private function generateUniqueFilename(string $originalFilename): void
118
    {
119 8
        $extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
120 8
        $filename = uniqid() . ($extension ? '.' . $extension : '');
0 ignored issues
show
Are you sure $extension of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
        $filename = uniqid() . ($extension ? '.' . /** @scrutinizer ignore-type */ $extension : '');
Loading history...
121 8
        $this->setFilename($filename);
122
    }
123
124
    /**
125
     * Delete file and throw exception if MIME type is invalid.
126
     */
127 8
    final protected function validateMimeType(): void
128
    {
129 8
        $path = $this->getPath();
130 8
        $mime = mime_content_type($path);
131 8
        if ($mime === false) {
132
            throw new Exception('Could not get mimetype for path: ' . $path);
133
        }
134
135 8
        if ($mime === 'image/svg') {
136
            $mime = 'image/svg+xml';
137
        }
138
139
        // Validate mimetype
140 8
        if (!in_array($mime, $this->getAcceptedMimeTypes(), true)) {
141
            unlink($path);
142
143
            throw new Exception('Invalid file type of: ' . $mime);
144
        }
145
146 8
        $this->mime = $mime;
147
    }
148
}
149