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']; |
|
|
|
|
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
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
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.