
Symfony:form Types Validation Skill
- 393 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:form-types-validation is a Claude Code skill that implements Symfony Form types, constraints, and validation groups with CSRF-safe submissions and reusable DTO mapping for developers building admin and public dat
About
symfony:form-types-validation is a Claude Code skill from makfly/superpowers-symfony that builds Symfony Form types with constraint validation, validation groups, and CSRF-protected submission handling. The skill creates reusable form classes for admin and public flows, maps submitted data to DTOs, and configures error handling for invalid input. Developers reach for this skill when Symfony applications need structured form intake beyond raw request parsing—especially when different validation rules apply per context through validation groups. Output includes FormType classes, constraint annotations or attributes, CSRF token integration, and consistent error response patterns.
- Custom FormType classes
- Constraint and validation groups
- DTO mapping and transformers
- CSRF and error presentation
- Reusable field widgets
Symfony:Form Types Validation by the numbers
- 393 all-time installs (skills.sh)
- Ranked #1,107 of 4,492 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonyform-types-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 393 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you add Symfony form validation groups?
Implement Symfony Form types, constraints, and validation groups for admin and public flows—CSRF-safe submissions with reusable DTO mapping and error handling.
Who is it for?
Symfony backend developers implementing admin or public forms who need constraint validation, validation groups, CSRF protection, and DTO mapping in one structured pass.
Skip if: API-only Symfony projects using JSON request bodies with Symfony Validator but no Form component, or frontend-heavy SPAs that handle validation entirely client-side.
When should I use this skill?
A Symfony app needs Form types with constraints, validation groups, CSRF-safe submissions, and DTO mapping for admin or public data entry flows.
What you get
Symfony FormType classes, constraint definitions, validation group configs, CSRF-safe handlers, and DTO mapping code.
- FormType classes
- Validation group configs
- DTO mapping code
Files
Form Types Validation (Symfony)
Use when
- Hardening access-control or validation boundaries.
- Aligning voters/security expressions with domain rules.
Default workflow
1. Map actor/resource/action decision matrix. 2. Implement voter/constraint logic at the right boundary. 3. Wire checks at controllers and API operations. 4. Test allowed/forbidden/invalid paths comprehensively.
Guardrails
- Avoid policy logic duplication across layers.
- Do not leak privileged state via error detail.
- Preserve explicit deny behavior for sensitive actions.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- Security boundary updates.
- Integration points enforcing decisions.
- Negative-path test results.
References
reference.mddocs/complexity-tiers.md
Reference
Symfony Forms and Validation
Basic Form Type
<?php
// src/Form/UserType.php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Full Name',
'attr' => ['placeholder' => 'John Doe'],
])
->add('email', EmailType::class, [
'label' => 'Email Address',
])
->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Confirm Password'],
'invalid_message' => 'The passwords do not match.',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}Validation Constraints
On Entity
<?php
// src/Entity/User.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\Entity]
#[UniqueEntity(fields: ['email'], message: 'This email is already registered.')]
class User
{
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Please enter your name.')]
#[Assert\Length(
min: 2,
max: 100,
minMessage: 'Name must be at least {{ limit }} characters.',
maxMessage: 'Name cannot exceed {{ limit }} characters.',
)]
private string $name;
#[ORM\Column(length: 255, unique: true)]
#[Assert\NotBlank]
#[Assert\Email(message: 'Please enter a valid email address.')]
private string $email;
#[ORM\Column]
#[Assert\NotBlank]
#[Assert\Length(min: 8, minMessage: 'Password must be at least {{ limit }} characters.')]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/',
message: 'Password must contain uppercase, lowercase, and numbers.',
)]
private string $password;
#[ORM\Column(type: 'date')]
#[Assert\NotNull]
#[Assert\LessThan('-18 years', message: 'You must be at least 18 years old.')]
private \DateTimeInterface $birthDate;
}On Form Type
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('website', UrlType::class, [
'constraints' => [
new Assert\Url(),
new Assert\Length(['max' => 255]),
],
])
->add('age', IntegerType::class, [
'constraints' => [
new Assert\Range(['min' => 18, 'max' => 120]),
],
])
;
}Validation Groups
<?php
// src/Entity/User.php
class User
{
#[Assert\NotBlank(groups: ['registration', 'profile'])]
private string $name;
#[Assert\NotBlank(groups: ['registration'])]
#[Assert\Email(groups: ['registration', 'profile'])]
private string $email;
#[Assert\NotBlank(groups: ['registration'])]
private string $password;
}
// src/Form/RegistrationType.php
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => ['registration'],
]);
}
// src/Form/ProfileType.php
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => ['profile'],
]);
}Custom Constraint
<?php
// src/Validator/Constraints/ValidPhoneNumber.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ValidPhoneNumber extends Constraint
{
public string $message = 'The phone number "{{ value }}" is not valid.';
public string $region = 'FR';
}
// src/Validator/Constraints/ValidPhoneNumberValidator.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class ValidPhoneNumberValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof ValidPhoneNumber) {
throw new UnexpectedTypeException($constraint, ValidPhoneNumber::class);
}
if (null === $value || '' === $value) {
return; // Let NotBlank handle empty values
}
// Custom validation logic
$phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
try {
$number = $phoneUtil->parse($value, $constraint->region);
if (!$phoneUtil->isValidNumber($number)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
} catch (\Exception $e) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}Usage:
#[ValidPhoneNumber(region: 'US')]
private string $phone;Data Transformers
<?php
// src/Form/DataTransformer/TagsTransformer.php
namespace App\Form\DataTransformer;
use App\Entity\Tag;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
class TagsTransformer implements DataTransformerInterface
{
public function __construct(
private EntityManagerInterface $em,
) {}
// Entity Collection -> String (for display)
public function transform(mixed $value): string
{
if ($value->isEmpty()) {
return '';
}
return implode(', ', $value->map(fn(Tag $tag) => $tag->getName())->toArray());
}
// String -> Entity Collection (from input)
public function reverseTransform(mixed $value): ArrayCollection
{
if (!$value) {
return new ArrayCollection();
}
$names = array_map('trim', explode(',', $value));
$tags = new ArrayCollection();
foreach ($names as $name) {
if (empty($name)) {
continue;
}
$tag = $this->em->getRepository(Tag::class)->findOneBy(['name' => $name]);
if (!$tag) {
$tag = new Tag();
$tag->setName($name);
$this->em->persist($tag);
}
$tags->add($tag);
}
return $tags;
}
}Usage in form:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('tags', TextType::class, [
'label' => 'Tags (comma-separated)',
])
;
$builder->get('tags')->addModelTransformer($this->tagsTransformer);
}Form Events
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('country', CountryType::class)
;
// Add state field dynamically based on country
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
$country = $data?->getCountry();
$this->addStateField($form, $country);
});
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
$country = $data['country'] ?? null;
$this->addStateField($form, $country);
});
}
private function addStateField(FormInterface $form, ?string $country): void
{
if ($country === 'US') {
$form->add('state', ChoiceType::class, [
'choices' => $this->usStates,
]);
} else {
$form->add('state', TextType::class, [
'required' => false,
]);
}
}Controller Usage
#[Route('/register', name: 'register')]
public function register(Request $request): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($user);
$this->em->flush();
$this->addFlash('success', 'Registration successful!');
return $this->redirectToRoute('home');
}
return $this->render('security/register.html.twig', [
'form' => $form,
]);
}HTTP 422 on invalid submit (Symfony 8.0+): pass the form (not
$form->createView()) torender(). When the form was submitted and is
invalid, Symfony automatically sets the response status to
422 Unprocessable Content (Turbo-compatible) instead of 200. The snippet
above already passes 'form' => $form, so it benefits from this.Recent Constraints
use Symfony\Component\Validator\Constraints as Assert;
class ChangePassword
{
// Strong password without a regex (weak | medium | strong | very_strong)
#[Assert\PasswordStrength(minScore: Assert\PasswordStrength::STRENGTH_STRONG)]
#[Assert\NotCompromisedPassword]
public string $newPassword;
// Run constraints in order, stopping at the first violation
#[Assert\Sequentially([
new Assert\NotBlank(),
new Assert\Length(min: 3),
new Assert\Regex('/^[a-z0-9_]+$/'),
])]
public string $username;
// Conditional validation
#[Assert\When(
expression: 'this.type === "company"',
constraints: [new Assert\NotBlank(), new Assert\Length(max: 14)],
)]
public ?string $vatNumber = null;
public string $type = 'individual';
}Multi-Step Forms (Symfony 8.1+ — verify)
// Build a form spread across several steps from a single object.
$form = $this->createFormFlowBuilder($task)->getForm();Custom Violation Mapper (Symfony 8.1+ — verify)
Override how constraint violations are mapped onto form fields by implementing ViolationMapperInterface; it is auto-registered as the form.violation_mapper service.
use Symfony\Component\Form\Util\ViolationMapperInterface;
class CustomViolationMapper implements ViolationMapperInterface
{
public function mapViolation(
ConstraintViolation $violation,
FormInterface $form,
bool $allowNonSynchronized = false,
): void {
// custom mapping
}
}Best Practices
1. Constraints on entities: Primary validation source 2. Form constraints for UI-specific validation: File uploads, etc. 3. Validation groups: Different rules for different contexts 4. Data transformers: Convert between formats 5. Custom constraints: Reusable business logic 6. Test validation: Unit test constraints
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- ./vendor/bin/phpunit --filter=Voter
- php bin/console debug:container security
- ./vendor/bin/phpstan analyse
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
FAQ
Does symfony:form-types-validation include CSRF protection?
symfony:form-types-validation implements CSRF-safe form submissions as part of Symfony Form type setup. The skill covers token integration alongside constraints, validation groups, and DTO mapping for admin and public flows.
What are Symfony validation groups used for in this skill?
symfony:form-types-validation configures validation groups so different constraints apply per admin or public context. Developers use groups when the same entity needs distinct validation rules across submission flows.