Passed
Push — master ( f08540...e0c380 )
by Jan
07:05
created

StructuralEntityType::modelReverseTransform()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 21
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Form\Type;
24
25
use App\Entity\Attachments\AttachmentType;
26
use App\Entity\Base\AbstractStructuralDBElement;
27
use App\Form\Type\Helper\StructuralEntityChoiceLoader;
28
use App\Services\Attachments\AttachmentURLGenerator;
29
use App\Services\Trees\NodesListBuilder;
30
use Doctrine\ORM\EntityManagerInterface;
31
use Symfony\Component\Form\AbstractType;
32
use Symfony\Component\Form\CallbackTransformer;
33
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
34
use Symfony\Component\Form\Event\PostSubmitEvent;
35
use Symfony\Component\Form\Event\PreSubmitEvent;
36
use Symfony\Component\Form\Exception\TransformationFailedException;
37
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
38
use Symfony\Component\Form\FormBuilderInterface;
39
use Symfony\Component\Form\FormEvents;
40
use Symfony\Component\Form\FormInterface;
41
use Symfony\Component\Form\FormView;
42
use Symfony\Component\OptionsResolver\Options;
43
use Symfony\Component\OptionsResolver\OptionsResolver;
44
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
45
use Symfony\Component\Validator\Constraints\IsNull;
46
use Symfony\Component\Validator\Constraints\Valid;
47
48
/**
49
 * This class provides a choice form type similar to EntityType, with the difference, that the tree structure
50
 * of the StructuralDBElementRepository will be shown to user.
51
 */
52
class StructuralEntityType extends AbstractType
53
{
54
    protected EntityManagerInterface $em;
55
    protected AttachmentURLGenerator $attachmentURLGenerator;
56
57
    /**
58
     * @var NodesListBuilder
59
     */
60
    protected $builder;
61
62
    public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, AttachmentURLGenerator $attachmentURLGenerator)
63
    {
64
        $this->em = $em;
65
        $this->builder = $builder;
66
        $this->attachmentURLGenerator = $attachmentURLGenerator;
67
    }
68
69
    public function buildForm(FormBuilderInterface $builder, array $options): void
70
    {
71
        $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) {
72
            //When the data contains non-digit characters, we assume that the user entered a new element.
73
            //In that case we add the new element to our choice_loader
74
75
            $data = $event->getData();
76
            if (null === $data || !is_string($data) || $data === "" || ctype_digit($data)) {
77
                return;
78
            }
79
80
            $form = $event->getForm();
81
            $options = $form->getConfig()->getOptions();
82
            $choice_loader = $options['choice_loader'];
83
            if ($choice_loader instanceof StructuralEntityChoiceLoader) {
84
                $choice_loader->setAdditionalElement($data);
85
            }
86
        });
87
88
       /* $builder->addEventListener(FormEvents::POST_SUBMIT, function (PostSubmitEvent $event) {
89
           $name =  $event->getForm()->getConfig()->getName();
90
           $data = $event->getForm()->getData();
91
92
           if ($event->getForm()->getParent() === null) {
93
               return;
94
           }
95
96
           $event->getForm()->getParent()->add($name, static::class, $event->getForm()->getConfig()->getOptions());
97
           $new_form = $event->getForm()->getParent()->get($name);
98
           $new_form->setData($data);
99
        });*/
100
101
102
103
        $builder->addModelTransformer(new CallbackTransformer(
104
            function ($value) use ($options) {
105
                return $this->modelTransform($value, $options);
106
            }, function ($value) use ($options) {
107
            return $this->modelReverseTransform($value, $options);
108
        }));
109
    }
110
111
    public function configureOptions(OptionsResolver $resolver): void
112
    {
113
        $resolver->setRequired(['class']);
114
        $resolver->setDefaults([
115
            'allow_add' => false,
116
            'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext
117
            'subentities_of' => null,   //Only show entities with the given parent class
118
            'disable_not_selectable' => false,  //Disable entries with not selectable property
119
            'choice_value' => function (?AbstractStructuralDBElement $element) {
120
                if ($element === null) {
121
                    return null;
122
                }
123
124
                if ($element->getID() === null) {
125
                    //Must be the same as the separator in the choice_loader, otherwise this will not work!
126
                    return $element->getFullPath('->');
127
                }
128
129
                return $element->getID();
130
            }, //Use the element id as option value and for comparing items
131
            'choice_loader' => function (Options $options) {
132
                return new StructuralEntityChoiceLoader($options, $this->builder, $this->em);
133
            },
134
            'choice_label' => function (Options $options) {
135
                return function ($choice, $key, $value) use ($options) {
136
                    return $this->generateChoiceLabels($choice, $key, $value, $options);
137
                };
138
            },
139
            'choice_attr' => function (Options $options) {
140
                return function ($choice, $key, $value) use ($options) {
141
                    return $this->generateChoiceAttr($choice, $key, $value, $options);
142
                };
143
            },
144
            'group_by' => function (AbstractStructuralDBElement $element)
145
            {
146
                //Show entities that are not added to DB yet separately from other entities
147
                if ($element->getID() === null) {
148
                    return 'New (not added to DB yet)';
149
                }
150
151
                return null;
152
            },
153
            'choice_translation_domain' => false, //Don't translate the entity names
154
        ]);
155
156
        //Set the constraints for the case that allow add is enabled (we then have to check that the new element is valid)
157
        $resolver->setNormalizer('constraints', function (Options $options, $value) {
158
            if ($options['allow_add']) {
159
                $value[] = new Valid();
160
            }
161
162
            return $value;
163
        });
164
165
        $resolver->setDefault('empty_message', null);
166
167
        $resolver->setDefault('controller', 'elements--structural-entity-select');
168
169
        $resolver->setDefault('attr', static function (Options $options) {
170
            $tmp = [
171
                'data-controller' => $options['controller'],
172
                'data-allow-add' => $options['allow_add'] ? 'true' : 'false',
173
            ];
174
            if ($options['empty_message']) {
175
                $tmp['data-empty-message'] = $options['empty_message'];
176
            }
177
178
            return $tmp;
179
        });
180
    }
181
182
183
    public function getParent(): string
184
    {
185
        return ChoiceType::class;
186
    }
187
188
    public function modelTransform($value, array $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

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

188
    public function modelTransform($value, /** @scrutinizer ignore-unused */ array $options)

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

Loading history...
189
    {
190
        return $value;
191
    }
192
193
    public function modelReverseTransform($value, array $options)
194
    {
195
        /* This step is important in combination with the caching!
196
           The elements deserialized from cache, are not known to Doctrinte ORM any more, so doctrine thinks,
197
           that the entity has changed (and so throws an exception about non-persited entities).
198
           This function just retrieves a fresh copy of the entity from database, so doctrine detect correctly that no
199
           change happened.
200
           The performance impact of this should be very small in comparison of the boost, caused by the caching.
201
        */
202
203
        if (null === $value) {
204
            return null;
205
        }
206
207
        //If the value is already in the db, retrieve it freshly
208
        if ($value->getID()) {
209
            return $this->em->find($options['class'], $value->getID());
210
        }
211
212
        //Otherwise just return the value
213
        return $value;
214
    }
215
216
    protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array
217
    {
218
        $tmp = [];
219
220
        //Disable attribute if the choice is marked as not selectable
221
        if ($options['disable_not_selectable'] && $choice->isNotSelectable()) {
222
            $tmp += ['disabled' => 'disabled'];
223
        }
224
225
        if ($choice instanceof AttachmentType) {
226
            $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()];
227
        }
228
229
        $level = $choice->getLevel();
230
        /** @var AbstractStructuralDBElement|null $parent */
231
        $parent = $options['subentities_of'];
232
        if (null !== $parent) {
233
            $level -= $parent->getLevel() - 1;
234
        }
235
236
        $tmp += [
237
            'data-level' => $level,
238
            'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null,
239
            'data-image' => $choice->getMasterPictureAttachment() ? $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), 'thumbnail_xs') : null,
240
        ];
241
242
        if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) {
243
            $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()];
244
        }
245
246
        return $tmp;
247
    }
248
249
    protected function getElementNameWithLevelWhitespace(AbstractStructuralDBElement $choice, $options, $whitespace = "&nbsp;&nbsp;&nbsp;"): string
250
    {
251
        /** @var AbstractStructuralDBElement|null $parent */
252
        $parent = $options['subentities_of'];
253
254
        /*** @var AbstractStructuralDBElement $choice */
255
        $level = $choice->getLevel();
256
        //If our base entity is not the root level, we need to change the level, to get zero position
257
        if (null !== $options['subentities_of']) {
258
            $level -= $parent->getLevel() - 1;
0 ignored issues
show
Bug introduced by
The method getLevel() does not exist on null. ( Ignorable by Annotation )

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

258
            $level -= $parent->/** @scrutinizer ignore-call */ getLevel() - 1;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
259
        }
260
261
        $tmp = str_repeat($whitespace, $level); //Use 3 spaces for intendation
262
        $tmp .= htmlspecialchars($choice->getName());
263
264
        return $tmp;
265
    }
266
267
    protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, $value, $options): string
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

267
    protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, /** @scrutinizer ignore-unused */ $value, $options): string

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

Loading history...
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

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

267
    protected function generateChoiceLabels(AbstractStructuralDBElement $choice, /** @scrutinizer ignore-unused */ $key, $value, $options): string

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

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

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

267
    protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, $value, /** @scrutinizer ignore-unused */ $options): string

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

Loading history...
268
    {
269
        return $choice->getName();
270
    }
271
}
272