glifery/entity-hidden-type-bundle
Symfony bundle that provides a hidden form type for Doctrine entities, letting forms submit an entity ID and resolve it back to the entity on submit—useful for compact forms and relationship fields without exposing full entity choice widgets.
The bundle registers form types for hidden inputs whose model data is a Doctrine entity: a single-entity type and — as of 2.1.0 — a collection/one-to-many variant. The surface area is still tiny: the Form/Type/EntityHiddenType.php and a Form/DataTransformer/EntityToId*Transformer. Read those two files first; the transformers differ between the single and collection paths, and that difference is where all the behaviour lives.
Install and enable:
composer require "glifery/entity-hidden-type-bundle:~2.1"
Flex-era config/bundles.php:
return [
Glifery\EntityHiddenTypeBundle\GliferyEntityHiddenTypeBundle::class => ['all' => true],
];
There is no bundle configuration to write — the extension exists but is effectively empty. The type is auto-wired against doctrine.orm.entity_manager; verify it registered with bin/console debug:form (check for the collection type too, not just the single one) and bin/console debug:container --tag=form.type.
First use case: a comment/reply form embedded on a page where the parent (Post, Thread, Company) is context, not user input. The controller sets the entity on the model, the form renders its id in a <input type="hidden">, and on submit the transformer hands you back the entity:
use Glifery\EntityHiddenTypeBundle\Form\Type\EntityHiddenType;
$comment = new Comment();
$comment->setPost($post); // so the hidden field has a value on render
$form = $this->createFormBuilder($comment)
->add('post', EntityHiddenType::class, ['class' => Post::class])
->add('body', TextareaType::class)
->add('save', SubmitType::class)
->getForm();
You must pass class explicitly — unlike EntityType, a hidden field cannot infer the target class from the form's data class, so there is no default. The same holds for the new collection type; expect to state class and (probably) an allow_add-style option explicitly.
Second use case (new in 2.1.0): a Post edit form where the tags collection is a hidden round-trip — the user edits the post body, the selected tags are carried through the payload as ids and rehydrated into a Collection of managed Tag entities. Look at the new collection transformer before wiring this: whether it expects a comma/JSON-encoded string or a CollectionType of child hidden fields determines your template, your JS and your PRE_SUBMIT guard.
Carry a fixed association through an edit form. Give the type the association property and let the transformer round-trip it, so $form->getData()->getPost() is the same managed instance after handleRequest() (Doctrine's identity map makes this hold within one EM).
Never render the field? Then it's not submitted. The classic first bug: people add the field but forget {{ form_widget(form.post) }} (or form_rest), the key is absent from the request, and the association silently becomes null. Render it, even though it's invisible. This bites harder with the collection type — you must loop {% for child in form.tags %}{{ form_widget(child) }}{% endfor %}, and a loop that skips one child silently drops that element from the hydrated collection instead of erroring.
Overwrite the value server-side in PRE_SUBMIT. This keeps the form self-contained while closing the IDOR hole:
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($post) {
$data = $event->getData();
if (is_array($data)) {
$data['post'] = $post->getId(); // ignore whatever the browser sent
$event->setData($data);
}
});
For the collection variant, override with an intersected whitelist rather than blindly trusting the array:
$data['tags'] = array_values(array_intersect(
(array) ($data['tags'] ?? []),
$allowedTagIds // ids the current user may attach
));
$event->setData($data);
One-to-many: decide who owns the relationship. The collection transformer rehydrates entities that already exist; it does not create them. So either (a) the ids are pre-existing rows and you only attach them ($post->addTag($tag) with the owning side on Post), or (b) you are abusing it to create rows, which it cannot do. Wire the inverse side in a POST_SUBMIT/controller step if the owning side is the child, otherwise Doctrine writes nothing and you'll spend an hour on "the form saved but the join table is empty."
Collections and nested forms. Works if each child model has its association set before rendering; allow_add prototypes will render an empty hidden value, which must be tolerated. Submitted collection indices can arrive out of order or sparse — normalise the array before handing it to the transformer, since a CollectionType will reindex but a raw hidden-string transformer won't.
API/JSON submits. Nothing special — the submitted payload just contains {"post": 42}, or for the collection form an array of ids. The transformer runs on the scalar id(s) exactly as it does for a normal HTML POST.
Combining with validation. Because the rehydrated entity is the entity, a Callback/custom constraint on the parent object (Post belongs to current user) runs naturally and is the correct place for the authorization check. For collections add All([new Valid()]) plus a Count constraint — without Count, posting tags: [] is a perfectly valid way to strip every tag.
Reuse the transformer on a plain HiddenType. If you only need this in one form, skip the bundle entirely — it's a ~15-line pattern, and for collections it's a CallbackTransformer mapping array<id> → array<Entity>:
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\CallbackTransformer;
$builder->add('post', HiddenType::class, ['compound' => false]);
$builder->get('post')->addModelTransformer(new CallbackTransformer(
fn (?Post $post) => $post?->getId(),
fn ($id) => $id ? $em->getRepository(Post::class)->find($id) : null
));
PRE_SUBMIT override above). The collection type widens this: the client controls the length of the array too, so they can add ids they don't own and remove ids they shouldn't be able to.orphanRemoval is a data-loss footgun. If the collection is cascade: [persist, remove] with orphanRemoval: true, a client-supplied array that omits an id makes Doctrine delete the child row. Treat the hidden collection as a replace-whole-set operation and validate/whitelist it accordingly, or map it to a non-owning side.TransformationFailedException, which Symfony converts to a form error on that field — and hidden fields' errors are rarely rendered. Set 'invalid_message' => 'Unknown post' and print form_errors(form.post), otherwise you'll debug a form that "just doesn't save." With a collection, one bad id can fail the whole field, so log the offending value in your PRE_SUBMIT listener.find() per identifier. Fine for a handful of tags, not for a 500-item multi-select; swap to a single findBy(['id' => $ids]) + array reorder if you hit it, or pre-warm the identity map.null vs []. An empty hidden input arrives as ''; an empty collection as [] or a missing key. Whether that becomes null, an empty ArrayCollection, or blows up the transformer depends on the transformer's implementation and your required/empty_data settings. Set 'required' => false for optional associations and add NotBlank/Count where mandatory — the collection type is the least forgiving here, so test the "user cleared the widget" path explicitly.id are where this package is weakest. Check the transformer source before using it on such an entity — you may need to subclass the type or roll the CallbackTransformer yourself. The new one-to-many support does not change this.glifery.form.type.* service definition to your doctrine.orm.<name>_entity_manager, or the transformer will find() in the wrong connection and return null for perfectly valid ids.EntityType is not a substitute when disabled is involved. Disabled fields are not submitted, so the association never round-trips. That gap is exactly what this bundle fills. (Disabled EntityType is fine if you set the association in the controller and only want it displayed.)tkuska/EntityHiddenTypeBundle) rather than the original author. Before upgrading, open the composer.json of the tag — not of master — and check the Symfony/Doctrine constraints; a merged PR on a dormant 2017-era bundle is not a maintenance commitment. If it fails debug:form on your Symfony version, vendor the type and transformer into your app (both single and collection variants are ~50 lines total) rather than fighting the bundle.debug:form <Type> to confirm the type and its options (do this for the collection type after upgrading — option names are the most likely breaking surface), debug:container glifery for the services, dump($form->get('post')->getErrors()) after isValid() fails, and for collections dump($form->get('tags')->getData()) before and after handleRequest(). If the submitted data is missing the key entirely, suspect a non-rendered widget before suspecting the transformer.How can I help you explore Laravel packages today?