nadialabs/nadia-form-bundle
Nadia Form Bundle offers reusable form components and validation helpers for PHP applications, simplifying form building, data binding, and error handling through a clean, framework-friendly API. Lightweight and easy to integrate into existing projects.
Confirm the framework first. Despite the description calling it a "Laravel/PHP package," this is a Symfony bundle (NadiaFormBundle, FormType classes). It only works inside a Symfony application with symfony/form installed. In Laravel there is no Form component, no bundle kernel registration, and no Twig form theme — the classes are inert. If you're on Laravel, treat this as MIT-licensed reference code to copy, not a package to require.
For a Symfony app that is the actual target:
composer require nadialabs/nadia-form-bundle
Register the bundle (Flex usually handles it; otherwise add manually):
// config/bundles.php
return [
// ...
NadiaLabs\NadiaFormBundle\NadiaFormBundle::class => ['all' => true],
];
Where to look first. The entire value is in src/Form/Type/. Read configureOptions() and buildView() on each class — those two methods tell you the accepted options and the Twig blocks the widget renders. If there is no src/Form/Type/ directory or it's empty of real types, stop here.
Discover what's actually registered:
php bin/console debug:form
php bin/console debug:form App\Form\MyType # inspect a single type's options
This is the equivalent of php artisan route:list for forms — the fastest way to see what the bundle exposes and what arguments each type accepts.
First use case. Don't scaffold a new form around it. Take one existing form in your app and replace a single awkward ->add() line with a bundle type — a date range, money amount, phone, or address block:
$builder
->add('name', TextType::class)
->add('budget', Nadia\MoneyType::class) // swap one field
->add('submittedAt', Nadia\DateRangeType::class);
Render, submit via handleRequest($request), and confirm isValid() and getData() round-trip correctly. If it renders and validates, you have a pattern worth repeating. If not, you've lost 20 minutes on a zero-star dependency instead of a day.
Use bundle types as leaf nodes. Reference them inside your own AbstractType ->add() calls. Don't fight them into composite forms you don't control — the moment you need custom behavior, extend the class rather than wiring around it.
Extend, don't fork. When a bundle type nearly fits, subclass it and override one method:
class CompanyMoneyType extends Nadia\MoneyType
{
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setDefault('currency', 'USD');
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
parent::buildView($view, $form, $options);
$view->vars['attribution'] = 'nadia-form-bundle';
}
}
This keeps the reuse benefit while isolating you from upstream changes — your subclass is the thing your team owns.
Standardize field behavior across CRUD screens. The real day-to-day win is consistency: every money input formats the same way, every phone field validates the same pattern. Register your derived types in your app (not the bundle) and use them everywhere. This is the pattern worth adopting even if you don't adopt the bundle.
Reuse the view fragment if the default markup fights Bootstrap/Tailwind. In templates/form/, add a theme block matching the type's getBlockPrefix():
{% use "form_div_layout.html.twig" %}
{% block nadia_money_widget %}
<div class="input-group">
<span class="input-group-text">{{ money_symbol }}</span>
{{ block('money_widget') }}
</div>
{% endblock %}
Wire it in config/packages/twig.yaml under form_themes. If a type doesn't provide its own block prefix or view vars, you're overriding the generic money_widget/text_widget — check buildView() before assuming.
Validate with constraints, not FormType logic. Put validation in constraints options or your DTO's @Assert annotations, not inside the type. Types stay reusable; validation stays testable and debuggable through debug:validator.
Test through the factory, not the type directly. Unit-test your derived types via TypeTestCase so you exercise the actual FormFactory, options, and view vars:
class CompanyMoneyTypeTest extends TypeTestCase
{
public function testCurrencyDefaultsToUsd(): void
{
$form = $this->factory->create(CompanyMoneyType::class);
$this->assertSame('USD', $form->getConfig()->getOption('currency'));
}
}
Wrong-framework hazard. If someone on the team read the description and tries to install this in a Laravel app, you get symfony/form, symfony/validator, and twig/twig in your dependency graph with nothing able to execute them. Reject that PR. The bundle is Symfony-only.
No tagged releases, no CI, no dependents. Pin to a specific commit hash, not a version constraint, in composer.json:
"nadialabs/nadia-form-bundle": "dev-main#abc1234"
An unpinned dev-main will drift silently and break your build the day the author force-pushes or the branch moves. If you can't pin, vendor the classes instead — you get the code, an attribution note, and a fork you control.
Symfony version drift is the likely breakage. A FormType compiled against Symfony 5 may reference option names or getBlockPrefix() signatures removed in 6/7. Check the package's composer.json symfony/form constraint against your app's composer show symfony/form before upgrading either side. If it's a major behind, expect to fix it yourself — there is no upstream issue tracker activity to rely on.
Missing or non-functional DependencyInjection/. Without a proper {Vendor}{Bundle}Extension and service config, the bundle may not register its types as services, so autowiring and constructor injection in the types will fail. Symptom: Cannot autowire service ... argument $xyz. Check whether services are declared in config/services.php (or Resources/config/services.yaml) before assuming the type is broken.
debug:form is your debugger. When an option "doesn't exist," when a widget renders empty, when isValid() throws — run debug:form <type> first. 90% of "the bundle is broken" turns out to be a wrong option name or a missing required default.
Clear the cache after any type change. Symfony caches form type metadata. php bin/console cache:clear before debugging a rendering issue you "just fixed."
Beware DataTransformers that touch the outside world. If a ChoiceLoader or transformer hits a repository or HTTP service, that type is stateful — it will behave unpredictably in tests and can cause N+1 queries on collection-heavy forms. Read buildForm() for anything that isn't pure option/constraint wiring.
Extension points to prefer when you don't want to fork: configureOptions() (defaults, allowed values), buildView() (Twig vars), finishView() (per-child tweaks), buildForm() (child composition). Anything that requires a FormTypeExtension is a hint the base type is being abused — back off.
Error messages are the first thing to break on Symfony upgrades. Symfony changed constraint message catalog keys between majors. If validation messages suddenly show the raw key, inspect translations/ and confirm the bundle ships (or you provide) the right validators.<locale>.xlf. If upstream is abandoned, you own that translation file.
Treat the license attribution as a real task. MIT requires you to retain the copyright notice. If you vendor classes, add a header comment with the original author and source URL, and note the copy in NOTICES or README. Cheap now, painful during a compliance review later.
How can I help you explore Laravel packages today?