-
-
Save alexander-schranz/b1cb8843bc54dc368001375fe6998e61 to your computer and use it in GitHub Desktop.
Experimental SymfonyForm ChoiceExtension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Extranet\Infrastructure\Symfony\Form\Extension; | |
use Symfony\Component\Form\AbstractTypeExtension; | |
use Symfony\Component\Form\CallbackTransformer; | |
use Symfony\Component\Form\ChoiceList\ChoiceList; | |
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
use Symfony\Component\Form\Extension\Core\Type\FormType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
use Symfony\Component\Form\FormInterface; | |
use Symfony\Component\Form\FormView; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
use Webmozart\Assert\Assert; | |
class ChoiceExtension extends AbstractTypeExtension | |
{ | |
/** | |
* @return iterable<string> | |
*/ | |
public static function getExtendedTypes(): iterable | |
{ | |
return [ChoiceType::class]; | |
} | |
public function buildView(FormView $view, FormInterface $form, array $options): void | |
{ | |
$view->vars['widget'] = $options['widget']; | |
$view->vars['widget_options'] = $options['widget_options']; | |
$items = []; | |
foreach ($view->vars['choices'] as $choiceValue => $choice) { | |
if ($options['choice_value']) { | |
if (\is_string($options['choice_value'])) { | |
$choiceValue = $choice->data->{$options['choice_value']}; | |
} elseif (\is_callable($options['choice_value'])) { | |
$choiceValue = $options['choice_value']($choice->data); | |
} | |
} | |
$items[$choiceValue] = $choice->data; | |
} | |
$view->vars['items'] = $items; | |
} | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'widget' => null, // null means it renders expanded or collapse default block of choice custom widgets can be defined | |
'widget_options' => [], // important to give more options to custom widgets | |
'item_value' => null, // similar to choice_value but it is which property $form->getData() returns when not configured whole item is returned | |
'item_loader_options' => [], // all options the item_loader requires | |
'item_loader' => null, // a callback method which load the items, callbed by a lazy choice_loader | |
]); | |
$resolver->addAllowedTypes('widget', ['string', 'null']); | |
$resolver->addAllowedTypes('widget_options', ['array']); | |
$resolver->addAllowedTypes('item_value', ['string', 'null']); | |
$resolver->addAllowedTypes('item_loader_options', ['string[]']); | |
$resolver->addAllowedTypes('item_loader', ['callable', 'null']); | |
$resolver->addNormalizer('choice_loader', function (OptionsResolver $options, mixed $choiceLoader) { | |
$itemLoader = $options['item_loader']; | |
if (null !== $itemLoader) { | |
Assert::null($choiceLoader, 'The "choice_loader" option cannot be used together with "item_loader".'); | |
$itemOptions = []; | |
foreach ($options['item_loader_options'] as $optionKey) { | |
$itemOptions[$optionKey] = $options[$optionKey]; | |
} | |
return ChoiceList::lazy($this, fn() => $options['item_loader']([], ...$itemOptions), $itemOptions); | |
} | |
return $choiceLoader; | |
}); | |
} | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
if ($options['item_value'] ?? null) { | |
$builder->addModelTransformer(new CallbackTransformer( | |
function ($values) use ($options): mixed { | |
if (null === $values || [] === $values) { | |
return $values; | |
} | |
$itemLoader = $options['item_loader']; | |
Assert::notNull($itemLoader); | |
$itemOptions = []; | |
foreach ($options['item_loader_options'] as $optionKey) { | |
$itemOptions[$optionKey] = $options[$optionKey]; | |
} | |
$items = $itemLoader((array) $values, ...$itemOptions); | |
if (true === $options['multiple']) { | |
return $items; | |
} | |
$firstKey = \array_key_first($items); | |
return $items[$firstKey] ?? null; | |
}, | |
function ($items) use ($options): mixed { | |
if (null === $items || [] === $items) { | |
return $items; | |
} | |
if (false === $options['multiple']) { | |
$item = $items; | |
return $item->{$options['item_value']}; | |
} | |
$values = []; | |
foreach ($items as $item) { | |
$values[] = $item->{$options['item_value']}; | |
} | |
return $values; | |
}, | |
)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{%- block choice_widget -%} | |
{% if widget %} | |
{{- block('choice_widget_' ~ widget) -}} {# if a widget is configured call the configured widget #} | |
{% elseif expanded %} | |
{{- block('choice_widget_expanded') -}} | |
{% else %} | |
{{- block('choice_widget_collapsed') -}} | |
{% endif %} | |
{%- endblock choice_widget -%} | |
{%- block choice_widget_table -%} | |
<div {{ block('widget_container_attributes') }}> | |
{# following parameters are the most important ones #} | |
{{ dump(widget) }} {# table #} | |
{{ dump(widget_options) }} {# any given widget_options #} | |
{{ dump(multiple) }} {# all widget need to support multi and single selection #} | |
{{ dump(full_name) }} {# name for the value / values #} | |
{{ dump(items) }} {# contains all items #} | |
{{ dump(value) }} {# contains current value (uuid) #} | |
{{ dump(data) }} {# contains current value data (contact) #} | |
<table> | |
{% set columns = widget_options[columns] %} | |
{%- for child in form %} | |
{% set item = items[child.vars.value] %} | |
<tr> | |
<td> | |
{{- form_widget(child) -}} | |
</td> | |
{%- for columnKey, column in columns -%} | |
<td> | |
{% if column.type == 'date' %} | |
{{ item[columnKey].format('Y-m-d') }} | |
{% else %} | |
{{ item[columnKey] }} | |
{% endif %} | |
</td> | |
{% endfor % | |
</tr> | |
{% endfor -%} | |
</table> | |
</div> | |
{%- endblock choice_widget_table -%} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Access\Infrastructure\Symfony\Form\ContactList; | |
use App\Access\Domain\Repository\ContactRepositoryInterface; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
class MyContactChoiceType extends AbstractType | |
{ | |
public function __construct(private readonly ContactRepositoryInterface $contactRepository) | |
{ | |
} | |
public function configureOptions(OptionsResolver $resolver) | |
{ | |
$resolver->setRequired('accountId'); | |
$resolver->setAllowedTypes('accountId', ['int']); | |
$resolver->setDefaults([ | |
'label' => 'app.contact', | |
'required' => true, | |
'expanded' => true, | |
'multiple' => false, | |
'widget' => 'table', | |
'choice_label' => function (object $contactList): string { | |
$label = $contactList->name; | |
if (null !== $contactList->description) { | |
$label .= ' - ' . $contactList->description; | |
} | |
return $label; | |
}, | |
'choice_value' => 'uuid', | |
'item_value' => 'uuid', | |
'item_loader_options' => ['accountId'], | |
'item_loader' => function ($values, int $accountId): iterable { | |
return $this->loadItems($values, $accountId); | |
}, | |
]); | |
} | |
/** | |
* @return array<string, object> | |
*/ | |
private function loadItems(array $values, int $accountId): array | |
{ | |
$filters = [ | |
'accountId' => $accountId, | |
'isPublic' => true, | |
]; | |
if ([] !== $values) { | |
$filters['uuids'] = $values; | |
} | |
$items = []; | |
foreach ($this->contactRepository->findFlatBy($filters, ['createdAt' => 'desc']) as $item) { | |
$items[$item['name']] = (object) $item; // we are not allowed to return arrays here | |
} | |
return $items; | |
} | |
public function getParent() | |
{ | |
return ChoiceType::class; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment