A form often has a second life.
First, someone uses it to enter or update information. Then, somewhere else in the application, someone needs to read that same information: on a confirmation screen, an internal detail page, or a review step before submitting a request.
The editable form is already there. Yet the read-only version frequently becomes another Twig template, with another list of fields, another set of labels, and sometimes another set of rules for displaying their values.
This is not a serious architectural failure. Writing a dedicated template is perfectly reasonable. But when an application contains many structured forms, a small amount of repeated presentation code can become a surprisingly persistent maintenance task.
I wanted to explore a modest alternative: what if a read-only view could reuse the information we have already given to Symfony's Form component?
That question led to FormSummaryBundle, a small open-source bundle with a deliberately limited purpose.
The duplication is easy to miss
Consider a delivery form with a carrier, status, pallet count, and a confirmation checkbox. The status field might be defined like this:
$builder->add('status', ChoiceType::class, [
'label' => 'Status',
'choices' => [
'Scheduled' => 'scheduled',
'Ready to load' => 'ready',
],
]);
The stored value is ready, but the label shown to the user is Ready to load.
Now imagine a separate read-only Twig template:
<dl>
<dt>Carrier</dt>
<dd>{{ delivery.carrier }}</dd>
<dt>Status</dt>
<dd>
{% if delivery.status == 'ready' %}
Ready to load
{% else %}
Scheduled
{% endif %}
</dd>
<dt>Pallets</dt>
<dd>{{ delivery.pallets }}</dd>
</dl>
There is nothing inherently wrong with this template. The problem is that part of the form's presentation contract now exists in two places.
If the status labels change, someone has to remember to update the detail view. If the form gains a field, the read-only version might need a matching change. If the application supports multiple languages, the two views must agree on translation domains and parameters. Formatting an empty value, a date, or a monetary amount introduces similar decisions.
This matters less for a single form. It matters more when you repeat the pattern across many forms and screens.
Symfony already gives us most of the ingredients
Symfony Forms are not simply a collection of HTML inputs. They hold information about field types, labels, choices, translation settings, and transformations between application data and values used for display.
When we call createView(), Symfony produces a FormView containing the information its rendering layer needs. And Symfony already exposes helpers such as field_label() and field_value(), along with an established form-theming system.
That distinction is important. FormSummaryBundle does not invent a new form model, replace Symfony's rendering system, or claim that this information was previously inaccessible.
It provides a convenience on top of those capabilities: a complete read-only representation, with common field types handled consistently, without manually mapping every field into a second template.
The entire basic API is one Twig call:
{{ form_summary(form) }}
The interesting part is not the line itself. It is how much existing form information that line can reuse.
A complete example
Here is a small form type using only standard Symfony fields.
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
final class DeliveryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('carrier', TextType::class, [
'label' => 'Carrier',
])
->add('status', ChoiceType::class, [
'label' => 'Status',
'choices' => [
'Scheduled' => 'scheduled',
'Ready to load' => 'ready',
],
])
->add('pallets', IntegerType::class, [
'label' => 'Pallets',
])
->add('confirmed', CheckboxType::class, [
'label' => 'Confirmed',
'required' => false,
]);
}
}
For this demonstration, the form is populated with an array. A real application could use its normal entity, DTO, or other data structure instead.
$form = $this->createForm(DeliveryType::class, [
'carrier' => 'Example Logistics',
'status' => 'ready',
'pallets' => 0,
'confirmed' => false,
]);
return $this->render('delivery/review.html.twig', [
'form' => $form->createView(),
]);
And the view is just:
{# templates/delivery/review.html.twig #}
{{ form_summary(form) }}
The default output is a semantic HTML description list, conceptually equivalent to:
<dl class="form-summary">
<div class="form-summary__row">
<dt>Carrier</dt><dd>Example Logistics</dd>
</div>
<div class="form-summary__row">
<dt>Status</dt><dd>Ready to load</dd>
</div>
<div class="form-summary__row">
<dt>Pallets</dt><dd>0</dd>
</div>
<div class="form-summary__row">
<dt>Confirmed</dt><dd>No</dd>
</div>
</dl>
Notice four things:
-
The choice label is preserved. The summary displays Ready to load, not the underlying
readyvalue. -
Zero stays zero. A pallet count of
0is not treated as missing data. - False is meaningful. The unchecked checkbox becomes No in the default English translation.
- Labels have a single source. The summary reuses the labels already configured on the form.
These are small details, but they are exactly the details that tend to diverge when we maintain separate representations by hand.
Why this is more than a loop over fields
A simple Twig loop could handle text fields quite well. Real Symfony forms, however, contain values whose display representation is not always their raw value.
Choice fields
A ChoiceType has its own mapping between submitted values and user-facing labels. A multiple-choice field may contain several selected values, grouped choices, or preferred choices. The summary uses existing choice views rather than inventing another label map.
This also applies to standard choice-derived types, including enum choices.
Dates and times
Symfony can render dates and times as single-text widgets or as compound fields containing individual components. The bundle works with those view representations. It respects the configured date pattern for supported compound dates and preserves the transformed view value for single-text fields.
It does not promise to localize every date into a new, independently chosen format. That is a responsibility worth keeping explicit.
Nested forms and collections
A form can contain a nested form or a collection of fields. The summary follows the existing FormView children and produces nested description lists. It summarizes the entries already in the form view, not collection prototypes intended for client-side additions.
Missing values
By default, an absent text value is displayed as an em dash. The caller can choose another placeholder:
{{ form_summary(form, {summary_empty: 'Not provided'}) }}
The same placeholder is used for missing components of partial compound dates and times. Values such as 0 and false remain distinct from an empty string or null.
The aim is consistency, not magic.
You do not have to summarize the whole form
Sometimes a full description list is unnecessary. Perhaps a card only needs one label/value pair, or a sentence needs the human-readable label of a selected choice.
Two smaller helpers are available:
<dl>
{{ form_summary_row(form.carrier) }}
{{ form_summary_row(form.status) }}
</dl>
<p>Current status: {{ form_summary_value(form.status) }}</p>
form_summary_row() renders one labeled row; form_summary_value() renders only the value.
Another useful detail: rendering a summary does not mark the original form view's fields as rendered. An application can still render editable widgets from the same view, before or after showing the summary.
That makes the approach useful for review panels alongside editable forms, without turning a read-only representation into a set of disabled inputs.
Two kinds of customization
I wanted customization to remain close to the conventions Symfony developers already know, rather than introduce another configuration language.
1. Control the value at the form level
For a field whose ordinary view value is not what we want to display, the bundle provides a summary_value option:
use Symfony\Component\Form\FormView;
$builder->add('reference', TextType::class, [
'summary_value' => static fn (FormView $view): string =>
'REF-'.strtoupper((string) $view->vars['value']),
]);
The callback runs when the value is summarized. It should be deterministic and free of side effects, because rendering the same summary again evaluates it again.
The default theme treats the result as text and escapes it. A custom scalar value is not a shortcut for injecting raw HTML.
2. Control the markup with a form theme
Symfony already has a mechanism for overriding how field types are rendered. The bundle adds summary-specific blocks to that mechanism.
For example, an application can provide a Twig form theme:
{# templates/form/summary.html.twig #}
{% block text_summary_value %}
<strong>{{ value }}</strong>
{% endblock %}
And apply it to the view:
{% form_theme form 'form/summary.html.twig' %}
{{ form_summary(form) }}
Summary-specific blocks include form_summary, form_summary_row, form_summary_value, and type-specific variations such as choice_summary_value.
The default theme does not load a stylesheet or replace normal editable widget blocks. A project can style the provided classes or override the markup to fit its design system.
There is no requirement to adopt Bootstrap, Tailwind, or another UI framework.
The security boundary is worth stating clearly
A read-only summary is not an authorization mechanism.
The bundle omits types such as password, hidden, file, repeated, and button fields. This keeps tokens, file inputs, and other inappropriate controls out of the default summary.
An application can also exclude a regular field:
$builder->add('internalNote', TextType::class, [
'summary' => false,
]);
But a field containing sensitive data may still be an ordinary TextType. No general-purpose form renderer can determine who is entitled to see that data.
The application must continue to enforce access control, select the correct data for the user, and decide what should appear on each page. Likewise, displaying a submitted form does not make its data valid: confirmation flows still need the application's normal submission and validation checks.
The default summary theme escapes text and choice labels. Custom themes remain application code, so developers should preserve escaping and avoid applying raw to untrusted data.
It is better to describe this boundary clearly than to promise safety a presentation package cannot provide.
When I would use it — and when I would not
I find this abstraction useful when a read-only screen closely follows the structure of an existing form: confirmation pages, internal inspection views, structured administrative details, or review sections in operational software.
It can also reduce routine maintenance. When a label or a choice changes in the form, the summary continues to use the form's current metadata, rather than a separately maintained mapping.
But I would not use it automatically on every detail page.
A dashboard with calculated KPIs, a domain-specific timeline, or a carefully designed customer-facing profile may deserve its own view model and template. A screen that combines data from several sources may not correspond to a form at all. Even for a simple form, five explicit lines of Twig may be more understandable than introducing a dependency.
The question is not whether the bundle can render the data. The question is whether the form's existing structure is the right structure for the read-only presentation.
If the answer is yes, reuse makes sense. If the answer is no, a dedicated template is often the cleaner design.
That trade-off is intentional.
What the bundle deliberately does not do
To keep its responsibilities narrow, FormSummaryBundle does not:
- validate submissions or enforce authorization;
- query entities to build a separate read model;
- persist or modify data;
- generate PDFs or send emails;
- implement a multi-step workflow; or
- automatically understand every possible custom compound field.
It renders from an already created FormView. Creating that form view may still involve normal Symfony transformations or choice loading, and a more specialized field can use summary_value or a custom summary theme block.
This is a small convenience layer, not another application framework.
Installation and compatibility
The package is published on Packagist:
composer require "alkin/form-summary-bundle:^1.0"
If your application does not register the bundle automatically, add it to config/bundles.php:
Alkin\FormSummaryBundle\FormSummaryBundle::class => ['all' => true],
There is no separate summary schema to configure. Create your Symfony form as usual, pass its FormView to Twig, and use form_summary().
Version 1.0.0 targets PHP 8.2+ and compatible Symfony 6.4, 7.4, and 8.x applications. The repository's CI matrix covers combinations of PHP 8.2–8.5 and Symfony 6.4, 7.4, 8.0, and 8.1, as well as lower supported dependency versions and installation of the exported package without development dependencies.
The test suite includes 94 PHPUnit tests with 214 assertions. Among the tested behaviors are escaping, translated choice labels, excluded fields, zero and false values, compound dates, and compatibility with editable form themes.
Tests cannot guarantee that every custom form behaves identically, but they provide a useful baseline for maintaining a small public API.
For the full option reference, supported field types, theme overrides, and known limitations, see the README.
A final thought
The most satisfying part of this project was not creating a new rendering system. It was realizing that Symfony already gives us much of what we need.
There is a place for dedicated, carefully designed templates. There is also a place for a small utility that removes a repeated task without changing how the rest of an application works.
When the form already describes the information we want to show, reusing that description can be simpler than writing it all again.
That is the entire idea behind FormSummaryBundle.
The source is available on GitHub, and the package is available on Packagist. Feedback is welcome, particularly from developers working with nested forms and custom field types. Those are often the places where a small abstraction reveals both its value and its limits.
Top comments (0)