DoctrineType::reset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of Biurad opensource projects.
5
 *
6
 * @copyright 2019 Biurad Group (https://biurad.com/)
7
 * @license   https://opensource.org/licenses/BSD-3-Clause License
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Flange\Database\Doctrine\Form\Type;
14
15
use Doctrine\Common\Collections\Collection;
16
use Doctrine\Persistence\ObjectManager;
17
use Flange\Database\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;
18
use Flange\Database\Doctrine\Form\ChoiceList\EntityLoaderInterface;
19
use Flange\Database\Doctrine\Form\ChoiceList\IdReader;
20
use Flange\Database\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
21
use Symfony\Component\Form\AbstractType;
22
use Symfony\Component\Form\ChoiceList\ChoiceList;
23
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
24
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
25
use Symfony\Component\Form\FormBuilderInterface;
26
use Symfony\Component\Form\FormEvent;
27
use Symfony\Component\Form\FormEvents;
28
use Symfony\Component\OptionsResolver\Options;
29
use Symfony\Component\OptionsResolver\OptionsResolver;
30
use Symfony\Contracts\Service\ResetInterface;
31
32
abstract class DoctrineType extends AbstractType implements ResetInterface
33
{
34
    protected ObjectManager $registry;
35
36
    /** @var IdReader[] */
37
    private array $idReaders = [];
38
39
    /** @var EntityLoaderInterface[] */
40
    private array $entityLoaders = [];
41
42
    /**
43
     * Creates the label for a choice.
44
     *
45
     * For backwards compatibility, objects are cast to strings by default.
46
     *
47
     * @internal This method is public to be usable as callback. It should not
48
     *           be used in user code.
49
     */
50
    public static function createChoiceLabel(object $choice): string
51
    {
52
        return (string) $choice;
53
    }
54
55
    /**
56
     * Creates the field name for a choice.
57
     *
58
     * This method is used to generate field names if the underlying object has
59
     * a single-column integer ID. In that case, the value of the field is
60
     * the ID of the object. That ID is also used as field name.
61
     *
62
     * @param string $value The choice value. Corresponds to the object's ID here.
63
     *
64
     * @internal This method is public to be usable as callback. It should not
65
     *           be used in user code.
66
     */
67
    public static function createChoiceName(object $choice, int|string $key, string $value): string
68
    {
69
        return \str_replace('-', '_', $value);
70
    }
71
72
    /**
73
     * Gets important parts from QueryBuilder that will allow to cache its results.
74
     * For instance in ORM two query builders with an equal SQL string and
75
     * equal parameters are considered to be equal.
76
     *
77
     * @param object $queryBuilder A query builder, type declaration is not present here as there
78
     *                             is no common base class for the different implementations
79
     *
80
     * @internal This method is public to be usable as callback. It should not
81
     *           be used in user code.
82
     */
83
    public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array
84
    {
85
        return null;
86
    }
87
88
    public function __construct(ObjectManager $registry)
89
    {
90
        $this->registry = $registry;
91
    }
92
93
    public function buildForm(FormBuilderInterface $builder, array $options): void
94
    {
95
        if ($options['multiple'] && \interface_exists(Collection::class)) {
96
            $builder
97
                ->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
98
                    $collection = $event->getForm()->getData();
99
                    $data = $event->getData();
100
101
                    // If all items were removed, call clear which has a higher
102
                    // performance on persistent collections
103
                    if ($collection instanceof Collection && 0 === \count($data)) {
104
                        $collection->clear();
105
                    }
106
                }, 5)
107
                ->addViewTransformer(new CollectionToArrayTransformer(), true)
108
            ;
109
        }
110
    }
111
112
    public function configureOptions(OptionsResolver $resolver): void
113
    {
114
        $choiceLoader = function (Options $options) {
115
            // Unless the choices are given explicitly, load them on demand
116
            if (null === $options['choices']) {
117
                // If there is no QueryBuilder we can safely cache
118
                $vary = [$this->registry, $options['class']];
119
120
                // also if concrete Type can return important QueryBuilder parts to generate
121
                // hash key we go for it as well, otherwise fallback on the instance
122
                if ($options['query_builder']) {
123
                    $vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getQueryBuilderPa...tions['query_builder']) targeting Flange\Database\Doctrine...erPartsForCachingHash() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
124
                }
125
126
                return ChoiceList::loader($this, new DoctrineChoiceLoader(
127
                    $this->registry,
128
                    $options['class'],
129
                    $options['id_reader'],
130
                    $this->getCachedEntityLoader(
131
                        $this->registry,
132
                        $options['query_builder'] ?? $this->registry->getRepository($options['class'])->createQueryBuilder('e'),
133
                        $options['class'],
134
                        $vary
135
                    )
136
                ), $vary);
137
            }
138
139
            return null;
140
        };
141
142
        $choiceName = function (Options $options) {
143
            // If the object has a single-column, numeric ID, use that ID as
144
            // field name. We can only use numeric IDs as names, as we cannot
145
            // guarantee that a non-numeric ID contains a valid form name
146
            if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
147
                return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
148
            }
149
150
            // Otherwise, an incrementing integer is used as name automatically
151
            return null;
152
        };
153
154
        // The choices are always indexed by ID (see "choices" normalizer
155
        // and DoctrineChoiceLoader), unless the ID is composite. Then they
156
        // are indexed by an incrementing integer.
157
        // Use the ID/incrementing integer as choice value.
158
        $choiceValue = function (Options $options) {
159
            // If the entity has a single-column ID, use that ID as value
160
            if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
161
                return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
162
            }
163
164
            // Otherwise, an incrementing integer is used as value automatically
165
            return null;
166
        };
167
168
        // Invoke the query builder closure so that we can cache choice lists
169
        // for equal query builders
170
        $queryBuilderNormalizer = function (Options $options, $queryBuilder) {
171
            if (\is_callable($queryBuilder)) {
172
                $queryBuilder = $queryBuilder($this->registry->getRepository($options['class']));
173
            }
174
175
            return $queryBuilder;
176
        };
177
178
        // Set the "id_reader" option via the normalizer. This option is not
179
        // supposed to be set by the user.
180
        $idReaderNormalizer = function (Options $options) {
181
            // The ID reader is a utility that is needed to read the object IDs
182
            // when generating the field values. The callback generating the
183
            // field values has no access to the object manager or the class
184
            // of the field, so we store that information in the reader.
185
            // The reader is cached so that two choice lists for the same class
186
            // (and hence with the same reader) can successfully be cached.
187
            return $this->getCachedIdReader($this->registry, $options['class']);
188
        };
189
190
        $resolver->setDefaults([
191
            'em' => null,
192
            'query_builder' => null,
193
            'choices' => null,
194
            'choice_loader' => $choiceLoader,
195
            'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
196
            'choice_name' => $choiceName,
197
            'choice_value' => $choiceValue,
198
            'id_reader' => null, // internal
199
            'choice_translation_domain' => false,
200
        ]);
201
202
        $resolver->setRequired(['class']);
203
        $resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
204
        $resolver->setNormalizer('id_reader', $idReaderNormalizer);
205
    }
206
207
    /**
208
     * Return the default loader object.
209
     */
210
    abstract public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): EntityLoaderInterface;
211
212
    public function getParent(): string
213
    {
214
        return ChoiceType::class;
215
    }
216
217
    public function reset(): void
218
    {
219
        $this->idReaders = [];
220
        $this->entityLoaders = [];
221
    }
222
223
    private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader
224
    {
225
        $hash = CachingFactoryDecorator::generateHash([$manager, $class]);
226
227
        if (isset($this->idReaders[$hash])) {
228
            return $this->idReaders[$hash];
229
        }
230
231
        $idReader = new IdReader($manager, $manager->getClassMetadata($class));
232
233
        // don't cache the instance for composite ids that cannot be optimized
234
        return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;
235
    }
236
237
    private function getCachedEntityLoader(ObjectManager $manager, object $queryBuilder, string $class, array $vary): EntityLoaderInterface
238
    {
239
        $hash = CachingFactoryDecorator::generateHash($vary);
240
241
        return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class));
242
    }
243
}
244