Skip to content

Instantly share code, notes, and snippets.

@alexander-schranz
Last active November 29, 2023 17:44
Show Gist options
  • Save alexander-schranz/b1cb8843bc54dc368001375fe6998e61 to your computer and use it in GitHub Desktop.
Save alexander-schranz/b1cb8843bc54dc368001375fe6998e61 to your computer and use it in GitHub Desktop.
Experimental SymfonyForm ChoiceExtension
<?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;
},
));
}
}
}
{%- 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 -%}
<?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