Alexandre Vaast
5 min readAug 29, 2017

L’automatisation de traitements, qu’ils soient Front ou Back, est un travail très important, parfois frustrant et souvent décevant.

Aujourd’hui beaucoup savent que vouloir tout automatiser est une utopie. Nombreux sont ceux qui se sont cassé les dents en voulant sur-automatiser leurs projets à tort et à travers.

C’est pourtant ce que nous allons voir aujourd’hui, car il est largement possible d’arriver à un résultat très satisfaisant, pourvu qu’on laisse de la place aux imprévus, et qu’on laisse la possibilité au développeur de ne pas suivre une route unique.

Symfony possède une structure logique et organisée qui permet de mettre en place une automatisation de traitement lourde, et pourtant facilement compréhensible par les développeurs utilisant le Framework.

Aujourd’hui nous allons voir comment nous pourrions automatiser la création de formulaire en fonction de l’entité qu’elle contiendra.

Ce type d’automatisation m’a énormément aidé dans l’élaboration d’un Bundle permettant la génération rapide d’un Back-Office, dans le cadre de nos projet pro chez Geoks.

Ce tutoriel n’est qu’un aperçu de ce que nous pouvons faire, afin de vous donner envie de vous adonner à la joie du templating !

Attaquons sans plus tarder !

Avant de mettre les mains dans le code, que voulons-nous comme résultat exactement ?

Nous voulons pouvoir générer un formulaire basique, en fonction d’une entité, et en nous laissant la possibilité de passer par un formulaire personnalisé si besoin.

Commençons par créer un service qui va lister les champs de notre entité :

public function getEntityFields($table)
{
$rowArr = [];

$cm = $this->em->getClassMetadata($table);
$rows = $cm->getFieldNames();
$rows = array_diff($rows, ['id']);

foreach ($rows as $row) {
$rowArr[$row] = $cm->getFieldMapping($row);
}

return $rowArr;
}

Définissons le service :

geoks_admin.entity_fields:
class:
Geoks\AdminBundle\Services\EntityFields
arguments:
-
"@doctrine.orm.entity_manager"

Je ne détaillerai pas ici toute la classe EntityFields, mais seulement les points importants.

Le service a besoin de l’entity manager pour fonctionner. Je rappel qu’il est très fortement déconseillé d’injecter le service container dans vos services. Pour plus de détails je vous conseil ce lien.

Dans ce service nous récupérons les champs de l’entité, en retirant l’id, puis nous stockons le mapping de ces champs dans un tableau.

Mais une entité contient également des relations :

public function getFieldsAssociations($table)
{
$rowAssos = [];

$cm = $this->em->getClassMetadata($table);
$rows = $cm->getAssociationNames();

foreach ($rows as $row) {
$targetClass = $cm->getAssociationTargetClass($row);
$reflection = new \ReflectionClass($targetClass);

if (!$reflection->isAbstract()) {
$findDatas = $this->em->getRepository($targetClass)
->findAll();

if (count($findDatas) > 0) {
$rowAssos[$row] = $cm->getAssociationMapping($row);
}
}
}

return $rowAssos;
}

Cette fonction va récupérer les relations de l’entité, si celle-ci ne sont pas nulles pour éviter de charger des entités inutilement.

Bien, nous avons la liste des champs concernés par notre formulaire, il faut maintenant créer la classe de formulaire :

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'allow_extra_fields' => true
));

$resolver->setRequired('service_container');
$resolver->setRequired('exceptions');
}

Commençons par la configuration de celui-ci, nous allons permettre l’insertion du service container et d’exceptions éventuelles.

NB : L’insertion du service container n’est pas une bonne chose ici non plus en théorie, nous pourrions améliorer cela en injectant uniquement les services dont nous avons besoin.

NB2 : Il existe de très nombreuses manières de procéder à ce stade. J’explique ici celle qui me semble la plus simple et la plus flexible.

public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var ContainerInterface $container */
$container = $options["service_container"];
$table = $options["data_class"];

$reader = new AnnotationReader();
$reflection = new \ReflectionClass($table);

$entityService = $container->get('geoks_admin.entity_fields');
$rowArr = $entityService->getFieldsName($table);
$rowAssos = $entityService->getFieldsAssociations($table);

foreach ($rowArr as $name => $field) {

if (isset($field["type"]) &&
!in_array($name, $options['exceptions'])) {
$typeOptions = $entityService->switchType($this->entityName, $name, $field["type"]);

$builder->add($name, $typeOptions['type'], $typeOptions['options']);
}

}

Vous remarquez la présence de la fonction switchType, qui va déterminer le type du champ. Cette fonction est dans notre service de gestion d’entité :

public function switchType($entityName, $name, $type)
{
$r = [];
$fieldName = $entityName . "." . $name;

switch ($type) {
case 'phone_number':
$r['type'] = PhoneNumberType::class;
$r['options'] = [
'default_region' => 'FR',
'format' => PhoneNumberFormat::INTERNATIONAL,
'attr' => [
'class' => 'control-animate'
]
];
break;
case 'integer':
$r['type'] = IntegerType::class;
$r['options'] = [
'label' => $fieldName,
'attr' => [
'class' => 'control-animate'
]
];
break;
case 'boolean':
$r['type'] = CheckboxType::class;
$r['options'] = [
'label' => $fieldName,
'attr' => [
'class' => 'checkbox-animate'
]
];
break;
case 'date':
$r['type'] = DateType::class;
$r['options'] = [
'label' => $fieldName,
'widget' => 'single_text',
'required' => false,
'format' => 'dd/MM/yyyy',
'attr' => [
'class' => 'control-animate datepicker'
]
];
break;
case 'datetime':
$r['type'] = DateTimeType::class;
$r['options'] = [
'label' => $fieldName,
'widget' => 'single_text',
'required' => false,
'format' => 'dd/MM/yyyy HH:mm',
'attr' => [
'class' => 'control-animate datetimepicker'
]
];
break;
case 'text':
$r['type'] = TextareaType::class;
$r['options'] = [
'label' => $fieldName,
'attr' => [
'class' => 'control-animate'
]
];
break;
case 'array':
$r['type'] = ChoiceType::class;
$r['options'] = [
'label' => $fieldName,
'choices' => null,
'expanded' => true,
'multiple' => true,
'attr' => [
'class' => 'control-animate choices-list'
]
];
break;
default:
$r['type'] = TextType::class;
$r['options'] = [
'label' => $fieldName,
'attr' => [
'class' => 'control-animate'
]
];
break;
}

return $r;
}

Il ne reste plus qu’à gérer les relations !

foreach ($rowAssos as $name => $class) {
if (!in_array($name, $options['exceptions'])) {

$typeOptions['options'] = [
'label' => $this->entityName . "." . $name,
'class' => $class['targetEntity'],
'required' => false,
'attr' => [
'class' => 'control-animate'
]
];

if ($annotation = $reader->getPropertyAnnotation($reflection->getProperty($name), "Symfony\\Component\\Validator\\Constraints\\NotNull")) {
$typeOptions['options']['required'] = true;
}

if ($class["type"] == 8) {
$typeOptions['options']['expanded'] = true;
$typeOptions['options']['multiple'] = true;
$typeOptions['options']['attr']['class'] = 'multiple';
$typeOptions['options']['label_attr']['class'] = 'label-multiple';

$builder->add($name, EntityMultipleType::class, $typeOptions['options']);

} elseif ($class["type"] != 4) {
$builder->add($name, EntityType::class, $typeOptions['options']);
}
}
}

Nous traitons toujours les exceptions. Attention au required, ainsi qu’au type de la relation. Ici nous traiterons les ManyToMany (type 8), et les ManyToOne (type 2).

Nous excluons le OneToMany (type 4), qui est un cas un peu plus compliqué à gérer pour le moment.

Il ne nous reste plus qu’à appeler le formulaire dans notre Controller.

/** ici j'ai pris la classe User comme exemple */
$form = $this->createForm(User::class, $entity, [
'attr' => [
'class' => "form-horizontal"
],
'action' => $this->generateUrl(sprintf('geoks_admin_users_create')),
'method' => 'POST',
'data_class' => User::class,
'service_container' => $this->get('service_container'),
"exceptions" => ['salt']
]);

Conclusion :

Avec ce système nous avons conçu une classe de formulaire, ainsi qu’un service, qui nous serviront dans de nombreux cas. Beaucoup de formulaires, notamment dans le cadre d’un back-office, sont longs et pénibles à écrire.