Passed
Push — dependabot/composer/doctrine/d... ( bcc5a8...f28646 )
by
unknown
75:30 queued 68:39
created

src/Common/Form/ImageType.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Common\Form;
4
5
use Common\Core\Model;
6
use Common\Doctrine\ValueObject\AbstractImage;
7
use stdClass;
8
use Symfony\Component\Form\AbstractType;
9
use Symfony\Component\Form\CallbackTransformer;
10
use Symfony\Component\Form\Exception\TransformationFailedException;
11
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
12
use Symfony\Component\Form\Extension\Core\Type\FileType as SymfonyFileType;
13
use Symfony\Component\Form\FormBuilderInterface;
14
use Symfony\Component\Form\FormEvent;
15
use Symfony\Component\Form\FormEvents;
16
use Symfony\Component\Form\FormInterface;
17
use Symfony\Component\Form\FormView;
18
use Symfony\Component\HttpFoundation\File\UploadedFile;
19
use Symfony\Component\OptionsResolver\Options;
20
use Symfony\Component\OptionsResolver\OptionsResolver;
21
use Symfony\Component\Validator\Constraints\File;
22
use Symfony\Component\Validator\Constraints\NotBlank;
23
use Symfony\Component\Validator\Constraints\Valid;
24
use Symfony\Component\Validator\Mapping\ClassMetadata;
25
use Symfony\Component\Validator\Validator\ValidatorInterface;
26
27
class ImageType extends AbstractType
28
{
29
    /**
30
     * @var ValidatorInterface
31
     */
32
    private $validator;
33
34
    public function __construct(ValidatorInterface $validator)
35
    {
36
        $this->validator = $validator;
37
    }
38
39
    public function buildForm(FormBuilderInterface $builder, array $options): void
40
    {
41
        if ($options['show_remove_image']) {
42
            $builder->add(
43
                'remove',
44
                CheckboxType::class,
45
                [
46
                    'required' => false,
47
                    'label' => $options['remove_image_label'],
48
                    'property_path' => 'pendingDeletion',
49
                ]
50
            );
51
        }
52
53
        $builder
54
            ->addEventListener(
55
                FormEvents::PRE_SET_DATA,
56
                function (FormEvent $event) use ($options) {
57
                    $imageIsEmpty = ($event->getData() === null || empty($event->getData()->getFileName()));
58
                    $required = $imageIsEmpty && $options['required'];
59
                    $fileFieldOptions = [
60
                        'label' => false,
61
                        'required' => $required,
62
                        'attr' => [
63
                            'accept' => $options['accept'],
64
                            'data-fork-cms-role' => 'image-field',
65
                        ],
66
                    ];
67
                    if ($required) {
68
                        $fileFieldOptions['constraints'] = [
69
                            new NotBlank(
70
                                ['message' => $options['required_image_error']]
71
                            ),
72
                        ];
73
                    }
74
                    $event->getForm()->add('file', SymfonyFileType::class, $fileFieldOptions);
75
                }
76
            )
77
            ->addModelTransformer(
78
                new CallbackTransformer(
79
                    function (AbstractImage $image = null) {
80
                        return $image;
81
                    },
82
                    function ($image) use ($options) {
83
                        if (!$image instanceof AbstractImage && !$image instanceof stdClass) {
84
                            throw new TransformationFailedException('Invalid class for the image');
85
                        }
86
87
                        $imageClass = $options['image_class'];
88
89
                        if (!$image instanceof AbstractImage) {
90
                            $image = $imageClass::fromUploadedFile($image->getFile());
0 ignored issues
show
The method getFile() does not exist on stdClass. It seems like you code against a sub-type of said class. However, the method does not exist in Symfony\Component\Proper...ests\Fixtures\Php7Dummy. Are you sure you never get one of those? ( Ignorable by Annotation )

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

90
                            $image = $imageClass::fromUploadedFile($image->/** @scrutinizer ignore-call */ getFile());
Loading history...
91
                        }
92
93
                        // return a clone to make sure that doctrine will do the lifecycle callbacks
94
                        return clone $image;
95
                    }
96
                )
97
            );
98
    }
99
100
    public function configureOptions(OptionsResolver $resolver): void
101
    {
102
        $resolver->setRequired(
103
            [
104
                'image_class',
105
                'show_preview',
106
                'show_remove_image',
107
                'remove_image_label',
108
                'required_image_error',
109
                'preview_image_directory',
110
                'accept',
111
            ]
112
        );
113
114
        $resolver->setDefaults(
115
            [
116
                'data_class' => AbstractImage::class,
117
                'empty_data' => function () {
118
                    return new class extends stdClass {
119
                        /** @var UploadedFile */
120
                        protected $file;
121
122
                        /** @var bool */
123
                        protected $pendingDeletion = false;
124
125
                        public function setFile(UploadedFile $file = null)
126
                        {
127
                            $this->file = $file;
128
                        }
129
130
                        public function getFile(): ?UploadedFile
131
                        {
132
                            return $this->file;
133
                        }
134
135
                        public function getPendingDeletion(): bool
136
                        {
137
                            return $this->pendingDeletion;
138
                        }
139
140
                        public function setPendingDeletion(bool $pendingDeletion)
141
                        {
142
                            $this->pendingDeletion = $pendingDeletion;
143
                        }
144
                    };
145
                },
146
                'preview_class' => 'img-thumbnail img-responsive',
147
                'show_preview' => true,
148
                'show_remove_image' => true,
149
                'required_image_error' => 'err.FieldIsRequired',
150
                'remove_image_label' => 'lbl.Delete',
151
                'preview_image_directory' => 'source',
152
                'accept' => 'image/*',
153
                'constraints' => [new Valid()],
154
                'error_bubbling' => false,
155
                'help_text_message' => 'msg.HelpImageFieldWithMaxFileSize',
156
                'help_text_argument' => function (Options $options) {
157
                    return $this->getUploadMaxFileSize($options['image_class']);
158
                },
159
            ]
160
        );
161
    }
162
163
    public function getBlockPrefix(): string
164
    {
165
        return 'fork_image';
166
    }
167
168
    public function buildView(FormView $view, FormInterface $form, array $options): void
169
    {
170
        $view->vars['show_preview'] = $options['show_preview'];
171
        $view->vars['show_remove_image'] = $options['show_remove_image'] && $form->getData() !== null
172
                                           && !empty($form->getData()->getFileName());
173
        // if you need to have an image you shouldn't be allowed to remove it
174
        if ($options['required']) {
175
            $view->vars['show_remove_image'] = false;
176
        }
177
        $imageIsEmpty = ($form->getData() === null || empty($form->getData()->getFileName()));
178
        $view->vars['required'] = $imageIsEmpty && $options['required'];
179
180
        $view->vars['preview_url'] = false;
181
        if ($form->getData() instanceof AbstractImage) {
182
            $view->vars['preview_url'] = $form->getData()->getWebPath($options['preview_image_directory']);
183
        }
184
185
        array_map(
186
            function ($optionName) use ($options, &$view) {
187
                if (array_key_exists($optionName, $options) && !empty($options[$optionName])) {
188
                    $view->vars[$optionName] = $options[$optionName];
189
                }
190
            },
191
            [
192
                'preview_class',
193
                'help_text_message',
194
                'help_text_argument',
195
            ]
196
        );
197
    }
198
199
    private function getMaxFileSizeServerValue(): ?int
200
    {
201
        $uploadMaxFileSize = ini_get('upload_max_filesize');
202
203
        if ($uploadMaxFileSize === false) {
204
            return null;
205
        }
206
207
        // reformat if defined as an integer
208
        if (is_numeric($uploadMaxFileSize)) {
209
            return $uploadMaxFileSize;
210
        }
211
212
        // reformat if specified in kB
213
        if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'K') {
214
            return mb_substr($uploadMaxFileSize, 0, -1) * 1000;
215
        }
216
217
        // reformat if specified in MB
218
        if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'M') {
219
            return mb_substr($uploadMaxFileSize, 0, -1) * 1000 * 1000;
220
        }
221
222
        // reformat if specified in GB
223
        if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'G') {
224
            return mb_substr($uploadMaxFileSize, 0, -1) * 1000 * 1000 * 1000;
225
        }
226
227
        return null;
228
    }
229
230
    private function getMaxFileSizeConstraintValue(string $imageClass): ?int
231
    {
232
        // use the metaData to find the file data
233
        /** @var ClassMetadata $metaData */
234
        $metaData = $this->validator->getMetadataFor($imageClass);
235
        $members = $metaData->getPropertyMetadata('file');
236
237
        // the constraints can be found in the property meta data members
238
        foreach ($members as $member) {
239
            $constraints = $member->getConstraints();
240
241
            if (count($constraints) === 1) {
242
                /** @var File $fileConstraint */
243
                $fileConstraint = $constraints[0];
244
245
                if ($fileConstraint instanceof File) {
246
                    return $fileConstraint->maxSize;
247
                }
248
            }
249
        }
250
251
        return null;
252
    }
253
254
    private function getUploadMaxFileSize(string $imageClass): ?string
255
    {
256
        $constraintValue = $this->getMaxFileSizeConstraintValue($imageClass);
257
        $serverValue = $this->getMaxFileSizeServerValue();
258
259
        if (!is_numeric($constraintValue) && !is_numeric($serverValue)) {
260
            return null;
261
        }
262
263
        // return the server value if the constraint value is to high
264
        if (is_numeric($constraintValue) && is_numeric($serverValue)) {
265
            if ($constraintValue < $serverValue) {
266
                return Model::prettyPrintFileSize($constraintValue);
267
            }
268
269
            return Model::prettyPrintFileSize($serverValue);
270
        }
271
272
        if (is_numeric($constraintValue)) {
273
            return Model::prettyPrintFileSize($constraintValue);
274
        }
275
276
        return Model::prettyPrintFileSize($serverValue);
277
    }
278
}
279