Failed Conditions
Push — master ( 34a070...6c283c )
by Adrien
05:56
created

Image::validateMimeType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 0
dl 0
loc 20
rs 9.8333
c 0
b 0
f 0
ccs 5
cts 7
cp 0.7143
crap 2.0932
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Model;
6
7
use Doctrine\ORM\Mapping as ORM;
8
use GraphQL\Doctrine\Annotation as API;
9
use Imagine\Filter\Basic\Autorotate;
10
use Imagine\Image\ImagineInterface;
11
use Psr\Http\Message\UploadedFileInterface;
12
13
/**
14
 * A card containing an image and some information about it
15
 *
16
 * @ORM\HasLifecycleCallbacks
17
 * @ORM\Entity(repositoryClass="Application\Repository\ImageRepository")
18
 */
19
class Image extends AbstractModel
20
{
21
    private const IMAGE_PATH = 'data/images/';
22
23
    /**
24
     * @var string
25
     * @ORM\Column(type="string", length=2000)
26
     */
27
    private $filename = '';
28
29
    /**
30
     * @var int
31
     * @ORM\Column(type="integer")
32
     */
33
    private $width = 0;
34
35
    /**
36
     * @var int
37
     * @ORM\Column(type="integer")
38
     */
39
    private $height = 0;
40
41
    /**
42
     * @var Bookable
43
     * @ORM\ManyToOne(targetEntity="Bookable")
44
     * @ORM\JoinColumns({
45
     *     @ORM\JoinColumn(onDelete="CASCADE")
46
     * })
47
     */
48
    private $bookable;
49
50
    /**
51
     * Set the image file
52
     *
53
     * @param UploadedFileInterface $file
54
     *
55
     * @throws \Exception
56
     */
57 1
    public function setFile(UploadedFileInterface $file): void
58
    {
59 1
        $this->generateUniqueFilename($file->getClientFilename());
0 ignored issues
show
Bug introduced by
It seems like $file->getClientFilename() can also be of type null; however, parameter $originalFilename of Application\Model\Image::generateUniqueFilename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

59
        $this->generateUniqueFilename(/** @scrutinizer ignore-type */ $file->getClientFilename());
Loading history...
60
61 1
        $path = $this->getPath();
62 1
        if (file_exists($path)) {
63
            throw new \Exception('A file already exist with the same name: ' . $this->getFilename());
64
        }
65 1
        $file->moveTo($path);
66
67 1
        $this->validateMimeType();
68 1
        $this->readFileInfo();
69 1
    }
70
71
    /**
72
     * Set filename (without path)
73
     *
74
     * @API\Exclude
75
     *
76
     * @param string $filename
77
     */
78 3
    public function setFilename(string $filename): void
79
    {
80 3
        $this->filename = $filename;
81 3
    }
82
83
    /**
84
     * Get filename (without path)
85
     *
86
     * @API\Exclude
87
     *
88
     * @return string
89
     */
90 3
    public function getFilename(): string
91
    {
92 3
        return $this->filename;
93
    }
94
95
    /**
96
     * Get absolute path to image on disk
97
     *
98
     * @API\Exclude
99
     *
100
     * @return string
101
     */
102 3
    public function getPath(): string
103
    {
104 3
        return realpath('.') . '/' . self::IMAGE_PATH . $this->getFilename();
105
    }
106
107
    /**
108
     * Automatically called by Doctrine when the object is deleted
109
     * Is called after database update because we can have issues on remove operation (like integrity test)
110
     * and it's preferable to keep a related file on drive before removing it definitely.
111
     *
112
     * @ORM\PostRemove
113
     */
114 1
    public function deleteFile(): void
115
    {
116 1
        $path = $this->getPath();
117 1
        if (file_exists($path) && is_file($path) && $this->getFilename() !== 'dw4jV3zYSPsqE2CB8BcP8ABD0.jpg') {
118 1
            unlink($path);
119
        }
120 1
    }
121
122
    /**
123
     * Get image width
124
     *
125
     * @return int
126
     */
127 2
    public function getWidth(): int
128
    {
129 2
        return $this->width;
130
    }
131
132
    /**
133
     * Set image width
134
     *
135
     * @API\Exclude
136
     *
137
     * @param int $width
138
     */
139 2
    public function setWidth(int $width): void
140
    {
141 2
        $this->width = $width;
142 2
    }
143
144
    /**
145
     * Get image height
146
     *
147
     * @return int
148
     */
149 2
    public function getHeight(): int
150
    {
151 2
        return $this->height;
152
    }
153
154
    /**
155
     * Set image height
156
     *
157
     * @API\Exclude
158
     *
159
     * @param int $height
160
     */
161 2
    public function setHeight(int $height): void
162
    {
163 2
        $this->height = $height;
164 2
    }
165
166
    /**
167
     * Generate unique filename while trying to preserver original extension
168
     *
169
     * @param string $originalFilename
170
     */
171 1
    private function generateUniqueFilename(string $originalFilename): void
172
    {
173 1
        $extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
174 1
        $filename = uniqid() . ($extension ? '.' . $extension : '');
175 1
        $this->setFilename($filename);
176 1
    }
177
178
    /**
179
     * Delete file and throw exception if MIME type is invalid
180
     *
181
     * @throws \Exception
182
     */
183 1
    private function validateMimeType(): void
184
    {
185 1
        $path = $this->getPath();
186 1
        $mime = mime_content_type($path);
187
188
        // Validate image mimetype
189
        $acceptedMimeTypes = [
190 1
            'image/bmp',
191
            'image/gif',
192
            'image/jpeg',
193
            'image/pjpeg',
194
            'image/png',
195
            'image/svg+xml',
196
            'image/webp',
197
        ];
198
199 1
        if (!in_array($mime, $acceptedMimeTypes, true)) {
200
            unlink($path);
201
202
            throw new \Exception('Invalid file type of: ' . $mime);
203
        }
204 1
    }
205
206
    /**
207
     * Read dimension and size from file on disk
208
     */
209 1
    private function readFileInfo(): void
210
    {
211 1
        global $container;
212 1
        $path = $this->getPath();
213
214
        /** @var ImagineInterface $imagine */
215 1
        $imagine = $container->get(ImagineInterface::class);
216 1
        $image = $imagine->open($path);
217
218
        // Auto-rotate image if EXIF says it's rotated
219 1
        $autorotate = new Autorotate();
220 1
        $autorotate->apply($image);
221 1
        $image->save($path);
222
223 1
        $size = $image->getSize();
224
225 1
        $this->setWidth($size->getWidth());
226 1
        $this->setHeight($size->getHeight());
227 1
    }
228
229
    /**
230
     * @return Bookable
231
     */
232
    public function getBookable(): Bookable
233
    {
234
        return $this->bookable;
235
    }
236
237
    /**
238
     * @param Bookable $bookable
239
     */
240 1
    public function setBookable(Bookable $bookable): void
241
    {
242 1
        $this->bookable = $bookable;
243 1
    }
244
}
245