-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
CommonPreConfigurator.php
229 lines (183 loc) · 8.8 KB
/
CommonPreConfigurator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
<?php
namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\AvatarField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use function Symfony\Component\String\u;
use function Symfony\Component\Translation\t;
use Symfony\Contracts\Translation\TranslatableInterface;
/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
final class CommonPreConfigurator implements FieldConfiguratorInterface
{
private PropertyAccessorInterface $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor)
{
$this->propertyAccessor = $propertyAccessor;
}
public function supports(FieldDto $field, EntityDto $entityDto): bool
{
// this configurator applies to all kinds of properties
return true;
}
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
$translationDomain = $context->getI18n()->getTranslationDomain();
// if a field already has set a value, someone has written something to
// it (as a virtual field or overwrite); don't modify the value in that case
$isReadable = true;
if (null === $value = $field->getValue()) {
try {
$value = null === $entityDto->getInstance() ? null : $this->propertyAccessor->getValue($entityDto->getInstance(), $field->getProperty());
} catch (AccessException) {
$isReadable = false;
}
$field->setValue($value);
$field->setFormattedValue($value);
}
$label = $this->buildLabelOption($field, $translationDomain, $context->getCrud()->getCurrentPage());
$field->setLabel($label);
$isRequired = $this->buildRequiredOption($field, $entityDto);
$field->setFormTypeOption('required', $isRequired);
$isSortable = $this->buildSortableOption($field, $entityDto);
$field->setSortable($isSortable);
$isVirtual = $this->buildVirtualOption($field, $entityDto);
$field->setVirtual($isVirtual);
$templatePath = $this->buildTemplatePathOption($context, $field, $entityDto, $isReadable);
$field->setTemplatePath($templatePath);
$doctrineMetadata = $entityDto->hasProperty($field->getProperty()) ? $entityDto->getPropertyMetadata($field->getProperty())->all() : [];
$field->setDoctrineMetadata($doctrineMetadata);
if (null !== $helpMessage = $this->buildHelpOption($field, $translationDomain)) {
$field->setHelp($helpMessage);
$field->setFormTypeOptionIfNotSet('help', $helpMessage);
$field->setFormTypeOptionIfNotSet('help_html', true);
}
if (!empty($field->getCssClass())) {
$field->setFormTypeOptionIfNotSet('row_attr.class', $field->getCssClass());
}
if (null !== $field->getTextAlign()) {
$field->setFormTypeOptionIfNotSet('attr.data-ea-align', $field->getTextAlign());
}
$field->setFormTypeOptionIfNotSet('label', $field->getLabel());
}
private function buildHelpOption(FieldDto $field, string $translationDomain): ?TranslatableInterface
{
$help = $field->getHelp();
if (null === $help || $help instanceof TranslatableInterface) {
return $help;
}
return '' === $help ? null : t($help, $field->getTranslationParameters(), $translationDomain);
}
/**
* @return TranslatableInterface|string|false|null
*/
private function buildLabelOption(FieldDto $field, string $translationDomain, ?string $currentPage)
{
// don't autogenerate a label for these special fields (there's a dedicated configurator for them)
if (FormField::class === $field->getFieldFqcn()) {
$label = $field->getLabel();
if ($label instanceof TranslatableInterface) {
return $label;
}
return empty($label) ? $label : t($label, $field->getTranslationParameters(), $translationDomain);
}
// if an Avatar field doesn't define its label, don't autogenerate it for the 'index' page
// (because the table of the 'index' page looks better without a header in the avatar column)
if (Action::INDEX === $currentPage && null === $field->getLabel() && AvatarField::class === $field->getFieldFqcn()) {
$field->setLabel(false);
}
// it field doesn't define its label explicitly, generate an automatic
// label based on the field's field name
if (null === $label = $field->getLabel()) {
$label = $this->humanizeString($field->getProperty());
}
if (empty($label)) {
return $label;
}
// don't translate labels in form-related pages because Symfony Forms translates
// labels automatically and that causes false "translation is missing" errors
if (\in_array($currentPage, [Crud::PAGE_EDIT, Crud::PAGE_NEW], true)) {
return $label;
}
if ($label instanceof TranslatableInterface) {
return $label;
}
return t($label, $field->getTranslationParameters(), $translationDomain);
}
private function buildSortableOption(FieldDto $field, EntityDto $entityDto): bool
{
if (null !== $isSortable = $field->isSortable()) {
return $isSortable;
}
return $entityDto->hasProperty($field->getProperty());
}
private function buildVirtualOption(FieldDto $field, EntityDto $entityDto): bool
{
return !$entityDto->hasProperty($field->getProperty());
}
private function buildTemplatePathOption(AdminContext $adminContext, FieldDto $field, EntityDto $entityDto, bool $isReadable): string
{
if (null !== $templatePath = $field->getTemplatePath()) {
return $templatePath;
}
// if field has a value set, don't display it as inaccessible (needed e.g. for virtual fields)
if (!$isReadable && null === $field->getValue()) {
return $adminContext->getTemplatePath('label/inaccessible');
}
if (null === $templateName = $field->getTemplateName()) {
throw new \RuntimeException(sprintf('Fields must define either their templateName or their templatePath. None given for "%s" field.', $field->getProperty()));
}
return $adminContext->getTemplatePath($templateName);
}
private function buildRequiredOption(FieldDto $field, EntityDto $entityDto): bool
{
if (null !== $isRequired = $field->getFormTypeOption('required')) {
return $isRequired;
}
// consider that virtual properties are not required
if (!$entityDto->hasProperty($field->getProperty())) {
return false;
}
$doctrinePropertyMetadata = $entityDto->getPropertyMetadata($field->getProperty());
// If at least one join column of an association field isn't nullable then the field is "required" by default, otherwise the field is optional
if ($entityDto->isAssociation($field->getProperty())) {
foreach ($doctrinePropertyMetadata->get('joinColumns', []) as $joinColumn) {
if (\array_key_exists('nullable', $joinColumn) && false === $joinColumn['nullable']) {
return true;
}
}
return false;
}
// TODO: check if it's correct to never make a boolean value required
// I guess it's correct because Symfony Forms treat NULL as FALSE by default (i.e. in the database the value won't be NULL)
if ('boolean' === $doctrinePropertyMetadata->get('type')) {
return false;
}
return !$doctrinePropertyMetadata->get('nullable');
}
private function humanizeString(string $string): string
{
$uString = u($string);
$upperString = $uString->upper()->toString();
// this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d')
// and other special labels which look better in uppercase
if ($uString->toString() === $upperString || \in_array($upperString, ['ID', 'URL'], true)) {
return $upperString;
}
return $uString
->replaceMatches('/([A-Z])/', '_$1')
->replaceMatches('/[_\s]+/', ' ')
->trim()
->lower()
->title(true)
->toString();
}
}