The List View ============= This document will cover the List view which you use to browse the objects in your system. It will cover configuration of the list itself and the filters you can use to control what's visible. Basic configuration ------------------- SonataAdmin Options that may affect the list view: .. code-block:: yaml # config/packages/sonata_admin.yaml sonata_admin: templates: list: '@SonataAdmin/CRUD/list.html.twig' action: '@SonataAdmin/CRUD/action.html.twig' select: '@SonataAdmin/CRUD/list__select.html.twig' list_block: '@SonataAdmin/Block/block_admin_list.html.twig' short_object_description: '@SonataAdmin/Helper/short-object-description.html.twig' batch: '@SonataAdmin/CRUD/list__batch.html.twig' inner_list_row: '@SonataAdmin/CRUD/list_inner_row.html.twig' base_list_field: '@SonataAdmin/CRUD/base_list_field.html.twig' pager_links: '@SonataAdmin/Pager/links.html.twig' pager_results: '@SonataAdmin/Pager/results.html.twig' Routes ------ You can disable listing entities by removing the corresponding routes in your Admin. For more detailed information about routes, see :doc:`routing`:: // src/Admin/PersonAdmin.php final class PersonAdmin extends AbstractAdmin { protected function configureRoutes(RouteCollectionInterface $collection): void { // Removing the list route will disable listing entities. $collection->remove('list'); } } Custom route ^^^^^^^^^^^^ Default route for a link is `show` (for `FieldDescriptionInterface::TYPE_MANY_TO_ONE` and `FieldDescriptionInterface::TYPE_ONE_TO_ONE`). Using this, the route can be customized as follows:: namespace App\Admin; use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Show\ShowMapper; final class MediaAdmin extends AbstractAdmin { protected function configureListFields(ListMapper $list): void { $list ->addIdentifier('field', null, [ 'route' => [ 'name' => 'edit' ] ]); } } Customizing the fields displayed on the list page ------------------------------------------------- You can customize the columns displayed on the list through the ``configureListFields`` method. Here is an example:: // ... protected function configureListFields(ListMapper $list): void { $list // addIdentifier allows to specify that this column // will provide a link to the entity // (edit or show route, depends on your access rights) ->addIdentifier('name') // you may specify the field type directly as the // second argument instead of in the options ->add('isVariation', FieldDescriptionInterface::TYPE_BOOLEAN) // if null, the type will be guessed ->add('enabled', null, [ 'editable' => true ]) // editable association field ->add('status', FieldDescriptionInterface::TYPE_CHOICE, [ 'editable' => true, 'class' => 'Vendor\ExampleBundle\Entity\ExampleStatus', 'choices' => [ 1 => 'Active', 2 => 'Inactive', 3 => 'Draft', ], ]) // editable multiple field ->add('winner', FieldDescriptionInterface::TYPE_CHOICE, [ 'editable' => true, 'multiple' => true, 'choices' => [ 'jury' => 'Jury', 'voting' => 'Voting', 'encouraging' => 'Encouraging', ], ]) // we can add options to the field depending on the type ->add('price', FieldDescriptionInterface::TYPE_CURRENCY, [ 'currency' => $this->currencyDetector->getCurrency()->getLabel() ]) // Here we specify which property is used to render the label of each entity in the list ->add('productCategories', null, [ 'associated_property' => 'name' // By default, sorting will be done on the associated property. // To sort on another property, add the following: 'sort_field_mapping' => [ 'fieldName' => 'weight', ], ]) // you may also use dotted-notation to access // specific properties of a relation to the entity ->add('image.name') // you may also use a custom accessor ->add('description1', null, [ 'accessor' => 'description' ]) ->add('description2', null, [ 'accessor' => function ($subject) { return $this->customService->formatDescription($subject); } ]) // You may also specify the actions you want to be displayed in the list ->add(ListMapper::NAME_ACTIONS, null, [ 'actions' => [ 'show' => [], 'edit' => [ // You may add custom link parameters used to generate the action url 'link_parameters' => [ 'full' => true, ] ], 'delete' => [], ] ]) ; } .. tip:: Edit and Delete actions are enabled in the default configuration. You can add your own! Default template file is: ``@SonataAdmin/CRUD/list__action_[ACTION_NAME].html.twig`` Options ^^^^^^^ .. note:: * ``(m)`` stands for mandatory * ``(o)`` stands for optional - ``type`` (m): defines the field type - mandatory for the field description itself but will try to detect the type automatically if not specified - ``template`` (o): the template used to render the field - ``label`` (o): the name used for the column's title - ``link_parameters`` (o): add link parameter to the related Admin class when the ``Admin::generateUrl`` is called - ``code`` (o): the method name to retrieve the related value (for example, if you have an ``array`` type field, you would like to show info prettier than `[0] => 'Value'`; useful when a getter is not enough). Notice: works with string-like types (string, text, html) - ``associated_property`` (o): property path to retrieve the "string" representation of the collection element, or a closure with the element as argument and return a string. - ``sort_field_mapping`` (o): property of the collection element to sort on. - ``identifier`` (o): if set to true a link appears on the value to edit the element Available types and associated options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------------------+---------------------+-----------------------------------------------------------------------+ | Type | Options | Description | +=======================================+=====================+=======================================================================+ | ``ListMapper::TYPE_ACTIONS`` | actions | List of available actions | + + + + | | edit | Name of the action (``show``, ``edit``, ``history``, ``delete``, etc) | + + + + | | link_parameters | Route parameters | +---------------------------------------+---------------------+-----------------------------------------------------------------------+ | ``ListMapper::TYPE_BATCH`` | | Renders a checkbox | +---------------------------------------+---------------------+-----------------------------------------------------------------------+ | ``ListMapper::TYPE_SELECT`` | | Renders a select box | +---------------------------------------+---------------------+-----------------------------------------------------------------------+ | ``FieldDescriptionInterface::TYPE_*`` | | See :doc:`Field Types ` | +---------------------------------------+---------------------+-----------------------------------------------------------------------+ Setting up custom action buttons ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can specify your own action buttons by setting up the 'template' option like so:: $listMapper ->add(ListMapper::NAME_ACTIONS, ListMapper::TYPE_ACTIONS, [ 'actions' => [ 'show' => [], 'edit' => [], 'delete' => ['template' => 'Admin/MyController/my_partial.html.twig'], //this twig file will be located at: templates/Admin/MyController/my_partial.html.twig ] ]); Symfony Data Transformers ^^^^^^^^^^^^^^^^^^^^^^^^^ If the model field has a limited list of values (enumeration), it is convenient to use a value object to control the available values. For example, consider the value object of moderation status with the following values: ``awaiting``, ``approved``, ``rejected``:: final class ModerationStatus { public const AWAITING = 'awaiting'; public const APPROVED = 'approved'; public const REJECTED = 'rejected'; private static $instances = []; private string $value; private function __construct(string $value) { if (!array_key_exists($value, self::choices())) { throw new \DomainException(sprintf('The value "%s" is not a valid moderation status.', $value)); } $this->value = $value; } public static function byValue(string $value): ModerationStatus { // limitation of count object instances if (!isset(self::$instances[$value])) { self::$instances[$value] = new static($value); } return self::$instances[$value]; } public function getValue(): string { return $this->value; } public static function choices(): array { return [ self::AWAITING => 'moderation_status.awaiting', self::APPROVED => 'moderation_status.approved', self::REJECTED => 'moderation_status.rejected', ]; } public function __toString(): string { return self::choices()[$this->value]; } } To use this Value Object in the _`Symfony Form`: https://symfony.com/doc/current/forms.html component, we need a _`Data Transformer`: https://symfony.com/doc/current/form/data_transformers.html :: use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; final class ModerationStatusDataTransformer implements DataTransformerInterface { public function transform($value): ?string { $status = $this->reverseTransform($value); return $status instanceof ModerationStatus ? $status->value() : null; } public function reverseTransform($value): ?ModerationStatus { if (null === $value || '' === $value) { return null; } if ($value instanceof ModerationStatus) { return $value; } try { return ModerationStatus::byValue($value); } catch (\Throwable $e) { throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); } } } For quick moderation of objects, it is convenient to do this on the page for viewing all objects. But if we just indicate the field as editable, then when editing we get in the object a string with the value itself (``awaiting``, ``approved``, ``rejected``), and not the Value Object (``ModerationStatus``). To solve this problem, you must specify the Data Transformer in the ``data_transformer`` field so that it correctly converts the input data into the data expected by your object:: // ... protected function configureListFields(ListMapper $list): void { $list ->add('moderation_status', 'choice', [ 'editable' => true, 'choices' => ModerationStatus::choices(), 'data_transformer' => new ModerationStatusDataTransformer(), ]) ; } Customizing the query used to generate the list ----------------------------------------------- You can customize the list query thanks to the ``configureQuery`` method:: protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface { $query = parent::configureQuery($query); $rootAlias = current($query->getRootAliases()); $query->andWhere( $query->expr()->eq($rootAlias . '.my_field', ':my_param') ); $query->setParameter('my_param', 'my_value'); return $query; } Customizing the sort order -------------------------- Configuring the default ordering column can be achieved by overriding the ``configureDefaultSortValues()`` method. All three keys ``DatagridInterface::PAGE``, ``DatagridInterface::SORT_ORDER`` and ``DatagridInterface::SORT_BY`` can be omitted:: // src/Admin/PostAdmin.php use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Datagrid\DatagridInterface; final class PostAdmin extends AbstractAdmin { // ... protected function configureDefaultSortValues(array &$sortValues): void { // display the first page (default = 1) $sortValues[DatagridInterface::PAGE] = 1; // reverse order (default = 'ASC') $sortValues[DatagridInterface::SORT_ORDER] = 'DESC'; // name of the ordered field (default = the model's id field, if any) $sortValues[DatagridInterface::SORT_BY] = 'updatedAt'; } // ... } .. note:: The ``DatagridInterface::SORT_BY`` key can be of the form ``mySubModel.mySubSubModel.myField``. .. note:: For UI reason, it's not possible to sort by multiple fields. However, this behavior can be simulate by adding some default orders in the ``configureQuery()`` method. The following example is using ``SonataAdminBundle`` with ``SonataDoctrineORMAdminBundle``:: // src/Admin/PostAdmin.php use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Datagrid\DatagridInterface; final class PostAdmin extends AbstractAdmin { // ... protected function configureDefaultSortValues(array &$sortValues): void { // display the first page (default = 1) $sortValues[DatagridInterface::PAGE] = 1; // reverse order (default = 'ASC') $sortValues[DatagridInterface::SORT_ORDER] = 'DESC'; // name of the ordered field (default = the model's id field, if any) $sortValues[DatagridInterface::SORT_BY] = 'updatedAt'; } protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface { $rootAlias = current($query->getRootAliases()); $query->addOrderBy($rootAlias.'.author', 'ASC'); $query->addOrderBy($rootAlias.'.createdAt', 'ASC'); return $query; } // ... } Filters ------- You can add filters to let user control which data will be displayed:: // src/Admin/PostAdmin.php use Sonata\AdminBundle\Datagrid\DatagridMapper; final class ClientAdmin extends AbstractAdmin { protected function configureDatagridFilters(DatagridMapper $datagrid): void { $datagrid ->add('phone') ->add('email') ; } // ... } All filters are hidden by default for space-saving. User has to check which filter he wants to use. To make the filter always visible (even when it is inactive), set the parameter ``show_filter`` to ``true``:: protected function configureDatagridFilters(DatagridMapper $datagrid): void { $datagrid ->add('phone') ->add('email', null, [ 'show_filter' => true ]) // ... ; } By default the template generates an ``operator`` for a filter which defaults to ``sonata_type_equal``. Though this ``operator_type`` is automatically detected it can be changed or even be hidden:: protected function configureDatagridFilters(DatagridMapper $datagrid): void { $datagrid ->add('foo', null, [ 'operator_type' => 'sonata_type_boolean' ]) ->add('bar', null, [ 'operator_type' => 'hidden' ]) // ... ; } If you don't need the advanced filters, or all your ``operator_type`` are hidden, you can disable them by setting ``advanced_filter`` to ``false``. You need to disable all advanced filters to make the button disappear:: protected function configureDatagridFilters(DatagridMapper $datagrid): void { $datagrid ->add('bar', null, [ 'operator_type' => 'hidden', 'advanced_filter' => false ]) // ... ; } Default filters ^^^^^^^^^^^^^^^ Default filters can be added to the datagrid values by using the ``configureDefaultFilterValues`` method. A filter has a ``value`` and an optional ``type``. If no ``type`` is given the default type ``is equal`` is used:: protected function configureDefaultFilterValues(array &$filterValues): void { $filterValues['foo'] = [ 'type' => ContainsOperatorType::TYPE_CONTAINS, 'value' => 'bar', ]; } Available types are represented through classes which can be found `here`_. Types like ``equal`` and ``boolean`` use constants to assign a choice of ``type`` to an ``integer`` for its ``value``:: namespace Sonata\Form\Type; final class EqualType extends AbstractType { const TYPE_IS_EQUAL = 1; const TYPE_IS_NOT_EQUAL = 2; } The integers are then passed in the URL of the list action e.g.: **/admin/user/user/list?filter[enabled][type]=1&filter[enabled][value]=1** This is an example using these constants for an ``boolean`` type:: use Sonata\Form\Type\EqualType; use Sonata\Form\Type\BooleanType; final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { protected function configureDefaultFilterValues(array &$filterValues): void { $filterValues['enabled'] = [ 'type' => EqualType::TYPE_IS_EQUAL, // => 1 'value' => BooleanType::TYPE_YES // => 1 ]; } } Please note that setting a ``false`` value on a the ``boolean`` type will not work since the type expects an integer of ``2`` as ``value`` as defined in the class constants:: namespace Sonata\Form\Type; final class BooleanType extends AbstractType { const TYPE_YES = 1; const TYPE_NO = 2; } This approach allow to create dynamic filters:: class PostAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { protected function configureDefaultFilterValues(array &$filterValues): void { // Assuming security context injected if (!$this->securityContext->isGranted('ROLE_ADMIN')) { $user = $this->securityContext->getToken()->getUser(); $filterValues['author'] = [ 'type' => EqualType::TYPE_IS_EQUAL, 'value' => $user->getId() ]; } } } .. note:: this is not a secure approach to hide posts from others. It's only an example for setting filters on demand! Callback filter ^^^^^^^^^^^^^^^ If you have the **SonataDoctrineORMAdminBundle** installed you can use the ``CallbackFilter`` filter type e.g. for creating a full text filter:: use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Filter\Model\FilterData; final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { protected function configureDatagridFilters(DatagridMapper $datagrid): void { $datagrid ->add('full_text', CallbackFilter::class, [ 'callback' => [$this, 'getFullTextFilter'], 'field_type' => TextType::class, ]); } public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): bool { if (!$data->hasValue()) { return false; } // Use `andWhere` instead of `where` to prevent overriding existing `where` conditions $query->andWhere($query->expr()->orX( $query->expr()->like($alias.'.username', $query->expr()->literal('%' . $data->getValue() . '%')), $query->expr()->like($alias.'.firstName', $query->expr()->literal('%' . $data->getValue() . '%')), $query->expr()->like($alias.'.lastName', $query->expr()->literal('%' . $data->getValue() . '%')) )); return true; } } The callback function should return a boolean indicating whether it is active. You can also get the filter type which can be helpful to change the operator type of your condition(s):: use Sonata\AdminBundle\Filter\Model\FilterData; use Sonata\Form\Type\EqualType; final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): bool { if (!$data->hasValue()) { return false; } $operator = $data->isType(EqualType::TYPE_IS_EQUAL) ? '=' : '!='; $query ->andWhere($alias.'.username '.$operator.' :username') ->setParameter('username', $data->getValue()) ; return true; } } Visual configuration -------------------- You have the possibility to configure your List View to customize the render without overriding to whole template. The following options are available: - `header_style`: Customize the style of header (width, color, background, align...) - `header_class`: Customize the class of the header - `collapse`: Allow to collapse long text fields with a "read more" link - `row_align`: Customize the alignment of the rendered inner cells - `label_icon`: Add an icon before label Example:: protected function configureListFields(ListMapper $list): void { $list ->add('id', null, [ 'header_style' => 'width: 5%; text-align: center', 'row_align' => 'center' ]) ->add('name', FieldDescriptionInterface::TYPE_STRING, [ 'header_style' => 'width: 35%' ]) ->add('description', FieldDescriptionInterface::TYPE_STRING, [ 'header_style' => 'width: 35%', 'collapse' => true ]) ->add('upvotes', null, [ 'label_icon' => 'fas fa-thumbs-up' ]) ->add('actions', null, [ 'header_class' => 'customActions', 'row_align' => 'right' ]) ; } If you want to customise the ``collapse`` option, you can also give an array to override the default parameters:: ->add('description', TextType::class, [ 'header_style' => 'width: 35%', 'collapse' => [ // height in px 'height' => 40, // content of the "read more" link 'more' => 'I want to see the full description', // content of the "read less" link 'less' => 'This text is too long, reduce the size', ] ]) If you want to show only the ``label_icon``:: ->add('upvotes', null, [ 'label' => false, 'label_icon' => 'fas fa-thumbs-up', ]) Mosaic view button ------------------ You have the possibility to show/hide mosaic view button. .. code-block:: yaml # config/packages/sonata_admin.yaml sonata_admin: # for hide mosaic view button on all screen using `false` show_mosaic_button: true You can show/hide mosaic view button using admin service configuration. You need to add option ``show_mosaic_button`` in your admin services: .. code-block:: yaml # config/services.yaml sonata_admin.admin.post: class: Sonata\AdminBundle\Admin\PostAdmin tags: - { name: sonata.admin, model_class: Sonata\AdminBundle\Entity\Post, manager_type: orm, group: admin, label: Post, show_mosaic_button: true } sonata_admin.admin.news: class: Sonata\AdminBundle\Admin\NewsAdmin tags: - { name: sonata.admin, model_class: Sonata\AdminBundle\Entity\News, manager_type: orm, group: admin, label: News, show_mosaic_button: false } Show Icons on Action Buttons ---------------------------- You can choose if the action buttons on the list-page show an icon, text or both. .. code-block:: yaml # config/packages/sonata_admin.yaml sonata_admin: options: # Choices are: text, icon or all (default) list_action_button_content: icon Checkbox range selection ------------------------ .. tip:: You can check / uncheck a range of checkboxes by clicking a first one, then a second one with shift + click. Displaying a non-model field ---------------------------- The list view can also display fields that are not part of the model class. In some situations you can add a new getter to your model class to calculate a field based on the other fields of your model:: // src/Entity/User.php public function getFullName(): string { return $this->getGivenName().' '.$this->getFamilyName(); } // src/Admin/UserAdmin.php protected function configureListFields(ListMapper $list) { $list->addIdentifier('fullName'); } In situations where the data are not available in the model or it is more performant to have the database calculate the value you can override the ``configureQuery()`` Admin class method to add fields to the result set. In ``configureListFields()`` these fields can be added using the alias given in the query. In the following example the number of comments for a post is added to the query and displayed:: // src/Admin/PostAdmin.php protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface { $query = parent::configureQuery($query); $query ->leftJoin('n.Comments', 'c') ->addSelect('COUNT(c.id) numberofcomments') ->addGroupBy('n'); return $query; } protected function configureListFields(ListMapper $list): void { $list->addIdentifier('numberofcomments'); } Lastly, you can also define your list fields as ``virtual``. This way, Sonata's FieldDescription will always return a value of null, as documented here: https://docs.sonata-project.org/projects/SonataAdminBundle/en/4.x/cookbook/recipe_virtual_field/ Combine this with configuring a custom template and you'll have a list column fully customizable in what it eventually renders. :: // src/Admin/PostAdmin.php protected function configureListFields(ListMapper $list) { $list->add('thisPropertyDoesNotExist', null, [ 'virtual_field' => true, 'template' => 'path/to/your/template.html.twig' ]); } .. _`SonataDoctrineORMAdminBundle Documentation`: https://docs.sonata-project.org/projects/SonataDoctrineORMAdminBundle/en/4.x/reference/list_field_definition/ .. _`here`: https://github.com/sonata-project/form-extensions/tree/1.x/src/Type Advance Usage ------------- Displaying sub model properties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. note:: This only makes sense when the prefix path is made of models, not collections. If you need to display only one field from a sub model or embedded object in a dedicated column, you can simply use the dot-separated notation:: namespace App\Admin; use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Show\ShowMapper; final class UserAdmin extends AbstractAdmin { protected function configureListFields(ListMapper $list): void { $list ->addIdentifier('id') ->add('firstName') ->add('lastName') ->add('address.street') ->add('address.ZIPCode') ->add('address.town') ; } } Custom template ^^^^^^^^^^^^^^^ If you need a specific layout for a row cell, you can define a custom template:: namespace App\Admin; use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Show\ShowMapper; final class MediaAdmin extends AbstractAdmin { protected function configureListFields(ListMapper $list): void { $list ->addIdentifier('id') ->add('image', FieldDescriptionInterface::TYPE_STRING, ['template' => '@SonataMedia/MediaAdmin/list_image.html.twig']) ->add('custom', FieldDescriptionInterface::TYPE_STRING, ['template' => '@SonataMedia/MediaAdmin/list_custom.html.twig']) ; } } The related template: .. code-block:: html+twig {% extends '@SonataAdmin/CRUD/base_list_field.html.twig' %} {% block field %}
{{ object.name }}
{{ object.providername}} : {{ object.width }}x{{ object.height }}
{% endblock %}