ruwork/polyfill-form-dti-bundle
Symfony bundle that polyfills the DateTimeImmutable form type for older framework versions, so you can use immutable date and time fields without upgrading Symfony. Drop-in integration for forms and applications.
ruwork/polyfill-form-dti-bundle is a Symfony bundle that polyfills DateTimeImmutable support in the Form component's date/time types (DateType, TimeType, DateTimeType). Core Symfony form types of that era were hard-wired to produce mutable \DateTime objects, so mapping a form to an entity or DTO with DateTimeImmutable properties failed with errors like "Expected argument of type DateTimeImmutable, DateTime given". This bundle backfills that behavior for Symfony 3.x/early-4.x projects.
Minimal setup:
composer require ruwork/polyfill-form-dti-bundle — expect 2018-era version constraints, so you may need a legacy PHP/Symfony matrix or --with-all-dependencies when resolving conflicts.new Ruwork\PolyfillFormDTIBundle\RuworkPolyfillFormDTIBundle() to AppKernel::registerBundles().config/bundles.php.bin/console cache:clear. Form type services are cached, so a stale container is the usual reason "nothing happened."bin/console debug:form DateTimeType and bin/console debug:container form.type.datetime. There are two possible designs, and they change your usage entirely:
Form/Type) that you must reference in your form builders.private DateTimeImmutable $startsAt; and a DateTimeType field. Before the bundle the submit fails; after, the form reverse-transforms the submitted string into a DateTimeImmutable.If the bundle overrides core services (transparent mode): change nothing in your form classes. Verify with debug:form that the data transformer now produces DateTimeImmutable, then add an integration test asserting assertInstanceOf(DateTimeImmutable::class, $form->getData()) so a future dependency bump can't silently regress you back to mutable.
If the bundle ships explicit types (opt-in mode): swap imports only in the form classes that need immutability:
// use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Ruwork\PolyfillFormDTIBundle\Form\Type\DateTimeType;
Keep the alias name identical so the diff stays a one-line-per-file change and migration to native Symfony later is a mechanical find-and-replace.
Custom form types: if you already subclass DateType/DateTimeType (very common for date-picker widgets), extend the bundle's type instead of the core type so you inherit the immutable transformer rather than bypassing it.
Per-field vs bundle-wide: if the bundle exposes an input option ('input' => 'datetime_immutable'), prefer setting it per field over relying on a global default — it keeps the immutability contract visible at the call site and limits blast radius.
Doctrine alignment: pair the form change with @ORM\Column(type="datetime_immutable") (Doctrine 2.6+). A DateTimeImmutable property backed by a datetime column is a classic source of conversion errors and confusing hydration behavior.
Validation: Symfony\Component\Validator\Constraints\DateTime accepts both mutable and immutable. But a Type constraint pinned to \DateTime::class will start failing — update those to DateTimeImmutable::class.
DTO / command pattern: this bundle pairs naturally with immutable command objects. Build a DTO with readonly-style DateTimeImmutable properties, let the form hydrate it directly, and skip the "copy mutable DateTime into immutable entity field" glue you'd otherwise write.
Testing workflow: fixtures should use DateTimeImmutable::from.../new DateTimeImmutable(...). In functional tests, submit through KernelBrowser rather than calling the type directly, so you exercise the transformer registered by the bundle.
Exit strategy: treat this as a scaffold, not a dependency. Modern Symfony supports immutable input natively via the input => 'datetime_immutable' option (or the immutable equivalents in later versions), so the migration is: drop the bundle, revert type imports to the core classes, add the input option, delete the bundle registration.
composer.json, and put a removal ticket in the backlog the day you add it.$date->modify('+1 day'), ->setTime(...), ->setDate(...) all return new instances on DateTimeImmutable. Any code that ignores the return value silently keeps the old date. Grep for ->modify(, ->setDate(, ->setTime(, ->add(, ->sub( on anything that now flows out of a form.form.type.datetime service, every date field in the app changes, including those rendered by third-party bundles (Sonata, EasyAdmin, custom pickers). Those bundles extend the core type and may bring their own mutable transformers back — you can end up with some fields immutable and some mutable in the same form.datetime vs datetime_immutable is not cosmetic: the DBAL type controls conversion. Mismatched mappings throw on persist/flush or hydrate the wrong class depending on Doctrine version.empty_data and non-required fields. An empty/blank submission can yield null or a default constructed object. Don't assume every non-null result is immutable — assert both branches in tests.model_timezone and view_timezone. Confirm the polyfill's transformer does too, or you'll get subtly shifted dates that only show up in production with non-UTC users.debug:form DateTimeType (which transformer?), debug:container form.type.datetime (which class?), then cache:clear. If the behavior flipped in one environment only, it's almost always a cached container or a different bundle registration order.vendor/. The bundle is small enough to fork if you need a long-term fix, but then you own the maintenance you were trying to avoid.input => 'datetime_immutable' support; this bundle only makes sense as a shim on a legacy branch you're not ready to upgrade.How can I help you explore Laravel packages today?