ruwork/reform
ruwork/reform is a Laravel/PHP package for defining forms as reusable objects, mapping request data, validating input, and transforming it into typed values. It helps keep form handling organized, consistent, and testable.
Reform is a Symfony-oriented form helper package from the Ruwork/Ruvent family. It is intended to sit alongside symfony/form and reduce the boilerplate you normally write when binding a form to a typed PHP object.
Minimal setup:
Install it next to a working Symfony app and the Form component:
composer require ruwork/reform:^0.2
The latest tagged release is 0.2.1, a patch that explicitly fixes compatibility with Symfony 4.1.2. That is a strong signal about the supported target line: this package is built for Symfony ~4.1, not 5/6/7. Pin an explicit constraint (^0.2 or 0.2.1 exactly) rather than relying on dev-master, otherwise a future force-push can break your build — and there is no newer tag to fall back to if it does.
If a Ruwork\ReformBundle\RuworkReformBundle (or similarly named) bundle class is exposed, register it in config/bundles.php:
Ruwork\ReformBundle\RuworkReformBundle::class => ['all' => true],
If the package ships only loose classes instead of a bundle, skip this step and just use the namespaced classes directly.
Skim the src/ tree first. In a package this small there is usually no docs site — the actual extension points live in the form type extensions and any ReformType / ReformExtension class. Read those before anything else.
First real use case: take a small DTO with a single property and build a form for it via FormFactoryInterface as you normally would, but swap the type you would have written (TextType, EntityType, etc.) for the Reform-provided equivalent. The payoff is supposed to be in typed mapping and consistent field configuration; verify that with a unit-test-style script on Symfony 4.1.x before wiring it into controllers. Doing this smoke test on the exact Symfony version you intend to run in production matters more than usual here, because the package's compatibility is pinned by patches like 0.2.1 rather than by a broad version range.
Inject the form factory, not the container. In a controller or handler, take Symfony\Component\Form\FormFactoryInterface and build forms through it. Reform's value is in the type classes it provides — don't instantiate them manually.
public function __construct(private FormFactoryInterface $forms) {}
public function handle(Request $request, MyDto $dto): ?RedirectResponse
{
$form = $this->forms->create(MyDtoType::class, $dto)->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// persist $dto
return $this->redirectToRoute('done');
}
return null;
}
Build reusable type classes around the DTO. Declare one AbstractType (or Reform equivalent) per DTO, configureOptions with data_class => MyDto::class, and compose Reform-provided child types. This is the workflow the package is aimed at — the day-to-day win is that each form type reads as a declarative mapping from DTO fields to widgets, with no procedural add() calls scattered in controllers.
Treat Reform types as drop-in children. They should slot into a standard FormBuilderInterface chain:
$builder
->add('email', ReformEmailType::class)
->add('name', ReformTextType::class, ['required' => false]);
If a Reform type does not accept the options you expect, fall back to the native Symfony type for that one field — mixing is fine because everything is still a FormTypeInterface.
Register form types as services. Tag them form.type and let autowiring resolve constructor dependencies (validator, repository, etc.). In services.yaml:
App\Form\:
resource: '../src/Form/'
tags: ['form.type']
Centralise translation of field labels and error messages. Reform reduces per-field configuration, so the remaining customisation typically moves into translation files under translations/validators.en.yaml and translations/messages.en.yaml. Keep labels keyed by <form>.<field> so the mapping stays predictable.
Test the form type, not the controller. Use TypeTestCase or a Symfony KernelTestCase with the form factory. Given the age of the package and the fact that compatibility is maintained by narrow point fixes, integration tests through the container are the only reliable way to catch BC breaks — unit tests against the raw classes are cheap and will survive.
FormTypeInterface changed signatures (buildForm, configureOptions) and OptionsResolver behaviour tightened. Check the composer.json require block before you invest in it, and be aware that even a bump to a later 4.1.x point release is untested by the maintainers.bin/console debug:container --tag=form.type shows nothing from the package, the bundle is not loaded and no types will be available.data_class and empty data. Symfony forms bind null data by default when a DTO has not been initialised, and many small form helpers assume a hydrated object. Always instantiate the DTO (new MyDto()) in the controller before createForm, otherwise you will hit "Unable to transform" / TransformationFailedException at submit time.FormEvents::POST_SUBMIT with a listener that dumps $event->getForm()->getData(). This is faster than reading through form extensions when a field silently becomes null.$builder->add(...) instead.composer.lock and consider a replace/provide shim if you later need to fork. Given the small surface area, forking to a local app/Reform/ namespace is often cheaper than fighting composer on a 2018-era 0.x package.form_row() blocks yourself, and any custom widget will need its own block_<type>_widget in your Twig theme.FormTypeExtensionInterface, that is the cleanest place to add project-wide defaults (widget classes, required icons) without forking. Read src/Extension/ — if it is missing, you are probably expected to write your own AbstractTypeExtension.How can I help you explore Laravel packages today?