2lenet/crudit-bundle
Symfony bundle to rapidly build configurable CRUD back offices with SB Admin layout. Provides list views with pagination, sorting, actions, exports and batch ops, plus datasources, filters, menus, workflows, maps, markdown and Twig helpers.
To create a crud, simply type this command in the console of your project:
php bin/console make:crudit
You will then be asked a series of questions :
That's it! Several files have been created:
src/Crudit/Config/EntityCrudConfig.phpsrc/Controller/Crudit/EntityController.phpsrc/Form/EntityType.phpsrc/Crudit/Datasource/Filterset/EntityFilterSet.phpsrc/Crudit/Datasource/EntityDatasource.php public function getDefaultSort(): array
{
return [['numeroLot', 'ASC']];
}
public function getDefaultAutocompleteSort(): ?array
{
return [['label', 'ASC']];
}
public function getChoicesNbItems(): array
{
return [15, 30, 45, 60];
}
Use setCardCssClass on a field to apply a CSS class to it in the card view.
Field::new('myField')->setCardCssClass('col-md-6');
It is possible to add actions to lists, list items and shows.
It is already possible to add action in a dropdown, adding ->setDropdown(true) to the action object.
public function getListActions(): array
{
$res[] = ListAction::new(
"action.export",
$this->getPath(CrudConfigInterface::EXPORT),
Icon::new("file-export")
)
->setModal("[@LleCrudit](https://github.com/LleCrudit)/modal/_export.html.twig")
->setConfig(
[
"export" => [Exporter::CSV, Exporter::EXCEL],
]
);
$res[] = ListAction::new(
'action.import.csv',
Path::new('import_lot_melange'),
Icon::new('download')
)->setDropdown(true);
$res[] = ListAction::new(
'action.batch.bons_livraisons',
Path::new('create_bons_livraisons'),
Icon::new('truck-loading')
)->setIsBatch();
return $res;
}
If you want to add a role for your action:
public function getListActions(): array
{
$res[] = ListAction::new(
'action.import.csv',
(Path::new('import_lot_melange'))->setRole('ROLE_IMPORT'),
Icon::new('download')
)->setDropdown(true);
return $res;
}
You can add conditions to actions, using the resource or not, in order to decide whether to display the action based on the data or the request.
To do this, simply use setDisplayIf (for the data) or setDisplayIfByRequest (for the request):
$actions[CrudConfigInterface::ACTION_SHOW] = ItemAction::new(
'action.show',
$this->getPath(CrudConfigInterface::SHOW),
Icon::new('search')
)
->setCssClass('btn btn-primary btn-sm crudit-action')
->setRole(sprintf('ROLE_%s_%s', $this->getName(), CrudConfigInterface::SHOW))
->setDisplayIf(fn(Resource $resource) => $resource->isActive())
->setDisplayIfByRequest(fn(Request $request) => $request->query->has('actif'));
You can configure editable conditions on lle\src\Dto\Field by adding
Field::new()->setEditInPlace(true)->setEditableIf(callback)
When we declare a new action in our page, we indicate the path of our method. This method must be in
src/Controller/Crudit/EntityController.php
In src/Form/EntityType.php. The form is a basic Symfony form. Several formtypes are available.
See : Form types

In src/Crudit/Datasource/Filterset/EntityFilterSet.php. See Filters

To enable striped tables in the lists, you must add this scss :
.crudit-list {
& > tbody > tr:nth-of-type(odd) {
--#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-striped-bg);
color: var(--#{$variable-prefix}table-striped-color);
}
}
:warning: Don't forget to add your new crud to the menu in src/Crudit/CrudMenu/AppMenuProvider.php:
LinkElement::new(
'menu.groupes',
Path::new('app_crudit_contact_index'),
Icon::new('/img/icons/contact.svg', Icon::TYPE_IMG),
"ROLE_CONTACT_INDEX"
)
It is possible to add totals to your list by adding the getTotalsFields method to the CrudConfig file.
public function getTotalFields(): array
{
return [
'montantHt' => [
'type' => CrudConfigInterface::SUM,
'field' => Field::new('montantHt', 'currency'),
],
'montantTtc' => [
'type' => CrudConfigInterface::SUM,
'field' => Field::new('montantTtc', 'currency'),
],
];
}
You can choose between 3 types of totals, AVERAGE, SUM and COUNT.
To use them, use the constants defined in the CrudConfigInterface file.
For computed totals that combine several columns, use EXPRESSION and provide a raw DQL expression:
public function getTotalFields(): array
{
return [
'totalAmount' => [
'type' => CrudConfigInterface::SUM,
'field' => Field::new('totalAmount', 'currency'),
],
'grossMargin' => [
'type' => CrudConfigInterface::EXPRESSION,
'expression' => 'SUM(root.totalAmount) - SUM(root.supplierCost)',
'field' => Field::new('grossMargin', 'currency'),
],
];
}
EXPRESSION totals are also computed per group when withSubtotals() is used.
:warning: Don't forget to specify the type of your field, as Crudit is unable to determine this itself.
You can also use totals on your sublist by adding getSubListTotalFields method to the CruditConfig file.
To configure route for DoctrineEntityField, you must set the route options:
Field::new('yourdoctrinentityfield', null, ['route' => 'your_route']);
Or:
Field::new('yourdoctrinentityfield')->setOptions(['route' => 'your_route']);
To configure autocomple route for DoctrineEntityField, you must set the setAutocompleteUrl method:
Field::new('yourdoctrinentityfield')->setAutocompleteUrl('your_autocomplete_url');
If you want to be able to select several items, you need to add the setMultiple method:
Field::new('yourdoctrinentityfield')->setAutocompleteUrl('your_autocomplete_url')->setMultiple(true);
If you want to add a role for your DoctrineEntityField:
Field::new('yourdoctrinentityfield', null, ['route' => 'your_route', 'routeRole' => 'YOUR_ROLE']);
Or:
Field::new('yourdoctrinentityfield')->setOptions(['route' => 'your_route', 'routeRole' => 'YOUR_ROLE']);
It is possible to refresh the value of a field in a list/show/sublist after modifying one using the fieldsToUpdate method.
To do this, you need to return an array containing each id of the element you want to refresh and the HTML code contained in that element.
The id of the element contain a part call 'yourfieldlabel', if the label of your field start with 'field.' or 'label.', Crudit automatically remove it. So if you've 'field.toto', 'yourefieldlabel' must be 'toto' but if you've 'text.toto' you'll have 'text.toto'.
public function fieldsToUpdate(int|string $id): array
{
$result = $this->em->getRepository(YourEntity::class)->find($id);
if (!$result) {
return [];
}
return [
'sublist-yourentity-' . $result->getId() . '-yourfieldid-yourfieldlabel' => $this->twig->render('the/template.html.twig', [
'view' => [
'field' => Field::new('yourfield')->setEditable('app_crudit_your_entity_editdata')
],
'resource' => $result,
'value' => $result->getYourField(),
'options' => [
"tableCssClass" => "text-end",
'decimals' => '2',
'decimal_separator' => ',',
'thousands_separator' => ' ',
],
]),
];
}
:warning: If you refresh a field which is also an editInPlace, you need to configure the
eipToUpdatemethod to re-enable the edit capability.
public function eipToUpdate(int|string $id): array
{
$result = $this->em->getRepository(YourEntity::class)->find($id);
if (!$result) {
return [];
}
return [
'sublist-yourentity-' . $result->getId() . '-yourfieldid-yourfieldlabel',
];
}
It is possible to auto refresh the page for the list and/or the show using the getListAutoRefresh/getShowAutoRefresh methods.
To do this, you need to return an integer that corresponds to the time interval (in seconds) between 2 refreshes.
public function getListAutoRefresh(): ?int
{
return 60;
}
public function getShowAutoRefresh(): ?int
{
return 60;
}
If you have a lot of information to put in your show brick, you can divide it into several groups, which will create several expandable elements (open by default).
public function getFields(string $key): array
{
switch ($key) {
case CrudConfigInterface::SHOW:
$fields = [
'title.group1' => [
$field1,
$field2,
$field3,
],
'title.group2' => [
$field4,
$field5,
],
];
break;
}
}
You can configure the number of field groups opened by default using the getShowNumberFieldGroupsOpened method.
public function getShowNumberFieldGroupsOpened(): ?int
{
return 2;
}
It is possible to visually group list rows by a field value using setRuptGroup. Two levels are supported.
public function getFields(string $key): array
{
switch ($key) {
case CrudConfigInterface::INDEX:
$fields = [
Field::new('groupField')->setRuptGroup(1),
Field::new('subGroupField')->setRuptGroup(2),
Field::new('fieldA'),
Field::new('fieldB'),
];
break;
}
}
Any sort configured on the list (user click or getDefaultSort) takes priority over the rupture sort by default. The rupture sort is appended after the existing sorts to guarantee consistent visual grouping within pages. See withRuptSortPriority() below if you need the rupture to always be the primary sort.
A group header row is always displayed, even when a group continues from the previous page.
:warning: Rupture fields must be database-mapped properties or Doctrine relations. Computed PHP methods are not supported.
When the rupture field is a date, use setRuptDateFormat to define the grouping granularity. This method accepts a PHP date format string and is used for SQL GROUP BY computation and for comparing successive rows to detect group changes.
Field::new('yourDateField')->setRuptGroup(1)->setRuptDateFormat('Y-m'), // group by month
Field::new('yourDateField')->setRuptGroup(1)->setRuptDateFormat('Y'), // group by year
Field::new('yourDateField')->setRuptGroup(1)->setRuptDateFormat('Y-m-d'),// group by day
Supported format characters: Y, y, m, n, d, j, H, h, i, s. Literal separators (-, /, , :) are passed through as-is.
To control how the date appears in the rupture header row, use setRuptDateDisplayFormat. It also accepts a PHP date format string and is applied only to the header label — it has no effect on SQL grouping or data cells.
Field::new('yourDateField')
->setRuptGroup(1)
->setRuptDateFormat('Y-m') // group by month (SQL + row comparison)
->setRuptDateDisplayFormat('m/Y'), // display as "06/2026" in the header
:warning: If
setRuptDateDisplayFormatis not set, the header falls back to the field's normal rendered value.
Use setRuptCssClass to apply Bootstrap (or custom) CSS classes to the rupture header <tr>. The default is no extra class.
Field::new('groupField')->setRuptGroup(1)->setRuptCssClass('bg-info text-center'),
By default the rupture field is hidden from the data rows (it only appears as a group header). Use setRuptHideFromList(false) to keep it visible as a normal column as well.
Field::new('groupField')->setRuptGroup(1)->setRuptHideFromList(false),
When the field value is null, the header displays "{field label} empty" using the translation key crudit.rupture.null_label. Override this with setRuptNullLabel:
Field::new('groupField')->setRuptGroup(1)->setRuptNullLabel('my.custom.translation.key'),
By default, user-defined sorts (column clicks, getDefaultSort) take precedence and the rupture sort is appended after them. Use withRuptSortPriority() on the rupture field to invert this: the rupture becomes the primary sort and existing sorts apply within each group.
Field::new('orderDate')
->setRuptGroup(1)
->setRuptDateFormat('Y-m')
->withRuptSortPriority(), // rupture is always the first ORDER BY
Without this option the rupture sort is appended last, so clicking a column header re-orders within the current groups but the groups themselves may not remain contiguous if the column sort conflicts with the rupture field.
When getTotalFields is defined, you can also display subtotals for each level-1 rupture group by adding withSubtotals() to the rupture field.
Field::new('groupField')->setRuptGroup(1)->withSubtotals(),
The subtotals use the same fields defined in getTotalFields. They are computed via a single SQL GROUP BY query and are therefore accurate across all pages, not just the visible page.
A subtotal row is displayed at the end of each group. If a group spans multiple pages, the same global subtotal is repeated on each page.
:warning:
withSubtotals()only works on level-1 rupture fields. Level-2 rupture groups do not have subtotals.
:warning: The datasource must extend
AbstractDoctrineDatasourcefor subtotals to be available.
Field::new('orderDate')
->setRuptGroup(1)
->setRuptDateFormat('Y-m')
->setRuptDateDisplayFormat('m/Y')
->setRuptHideFromList(false)
->setRuptCssClass('bg-info text-center')
->setRuptNullLabel('my.custom.null_key') // optional
->withRuptSortPriority() // optional — rupture sorts before any other sort
->withSubtotals(),
How can I help you explore Laravel packages today?