survos/api-grid-bundle

incorporate DatatablesNet, using twig and stimulus, twig_component requires ^2.2

Fund package maintenance!
kbond

Installs: 4 636

Dependents: 0

Suggesters: 1

Security: 0

Stars: 2

Watchers: 2

Forks: 0

Open Issues: 2

Type:symfony-bundle

pkg:composer/survos/api-grid-bundle

2.0.112 2026-02-17 15:22 UTC

This package is auto-updated.

Last update: 2026-02-18 01:15:47 UTC


README

Render a spreadsheet-like, server-driven DataTables.net grid from an API Platform collection endpoint.

This bundle used to be central across multiple projects. Today the primary goal is simple Doctrine browsing (sorting, searching, filtering) using a single Twig component.

Meilisearch support exists, but the recommended starting point is Doctrine + API Platform.

Install

composer req survos/api-grid-bundle

Quick Start (Doctrine)

Doctrine setup

This bundle is designed to point DataTables at an API Platform collection endpoint. It uses Hydra JSON-LD and a custom paginator that accepts limit and offset.

1) Add QueryBuilder helper to your repository

This enables facet counts for SearchPanes.

use Survos\CoreBundle\Traits\QueryBuilderHelperInterface;
use Survos\CoreBundle\Traits\QueryBuilderHelperTrait;

class OfficialRepository extends ServiceEntityRepository implements QueryBuilderHelperInterface
{
    use QueryBuilderHelperTrait;
}

2) Define columns

{% set columns = [
    'code',
    'description',
    {name: 'countryCode', sortable: true, browsable: true, searchable: true},
    {name: 'privacyPolicy', browsable: true},
    {name: 'projectLocale', browsable: true},
] %}

By default, sortable, browsable, and searchable are false.

3) Add filters to the resource

use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use Survos\ApiGrid\Api\Filter\MultiFieldSearchFilter;
use Survos\ApiGrid\Api\Filter\FacetsFieldSearchFilter;

#[ApiFilter(MultiFieldSearchFilter::class, properties: ['name', 'code'])]
#[ApiFilter(FacetsFieldSearchFilter::class, properties: ['gender', 'state'])]
#[ApiFilter(OrderFilter::class, properties: ['id', 'countryCode'])]

4) Choose the API route

If your resource has a single collection operation, the grid can infer it. If there are multiple collection operations, pass the apiRoute (operation name) explicitly.

<twig:api_grid class="App\\Entity\\Official" apiRoute="api_officials_get_collection" />

To discover operation names, run:

bin/console debug:router | grep api_

Or bypass discovery entirely and pass a URL:

<twig:api_grid apiGetCollectionUrl="/api/officials.jsonld" />

5) Pagination settings

Datatables uses limit and offset. Ensure API Platform allows client pagination:

api_platform:
  collection:
    pagination:
      client_items_per_page: true
      client_enabled: true

The recommended approach is:

  • pass the entity class from your controller (avoid class strings in Twig)
  • pass an explicit API Platform collection URL (apiGetCollectionUrl)

1) Add a named collection operation constant

Define a route name constant on your ApiResource (this keeps things easy to reason about):

// src/Entity/Video.php

use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
#[GetCollection(name: self::DOCTRINE_ROUTE)]
class Video
{
    public const DOCTRINE_ROUTE = 'api-video';
}

2) Compute the collection URL in your controller

// src/Controller/VideoController.php

use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IriConverterInterface;
use App\Entity\Video;

#[Route(path: '/video/browse', name: 'video_browse', methods: ['GET'])]
public function browse(IriConverterInterface $iriConverter): Response
{
    $apiGetCollectionUrl = $iriConverter->getIriFromResource(
        Video::class,
        operation: new GetCollection(name: Video::DOCTRINE_ROUTE)
    );

    return $this->render('video/browse.html.twig', [
        'class' => Video::class,
        'apiGetCollectionUrl' => $apiGetCollectionUrl,
    ]);
}

3) Render the grid in Twig

{# templates/video/browse.html.twig #}

{% set columns = [
    col('youtubeId'),
    col(name: 'year', sortable: true, browsable: true),
    col(name: 'title', searchable: true, sortable: true),
] %}

<twig:api_grid
    :class="class"
    :apiGetCollectionUrl="apiGetCollectionUrl"
    :columns="columns"
    :caller="_self"
>
    <twig:block name="youtubeId">
        <a target="_blank" href="https://www.youtube.com/watch?v={{ row.youtubeId }}">
            <img src="{{ row.thumbnailUrl }}" height="60" alt="{{ row.youtubeId }}"/>
        </a>
    </twig:block>
</twig:api_grid>

Notes:

  • browsable: true marks a field as a SearchPanes facet.
  • :caller="_self" enables inline <twig:block name="..."> templates for custom rendering.

EasyAdmin / Sidebar Layouts: ColumnControl

SearchPanes are great, but the left-hand facet sidebar can clash with admin layouts that already have a sidebar (e.g. EasyAdmin).

Enable DataTables ColumnControl instead:

<twig:api_grid
    :class="class"
    :apiGetCollectionUrl="apiGetCollectionUrl"
    :columns="columns"
    :caller="_self"
    :searchPanes="false"
    :columnControl="true"
/>

How it works:

  • browsable: true columns become ColumnControl searchList dropdowns.
  • Selections are sent back as facet_filter[] so the existing Doctrine filter path is reused.

Backend Setup

Search (global search box)

The JS grid sends a global query as ?search=.... Enable it with:

use ApiPlatform\Metadata\ApiFilter;
use Survos\ApiGridBundle\Api\Filter\MultiFieldSearchFilter;

#[ApiFilter(MultiFieldSearchFilter::class, properties: ['title', 'description'])]

Sorting

DataTables sends ordering as order[field]=asc|desc, so add API Platform's OrderFilter:

use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;

#[ApiFilter(OrderFilter::class, properties: ['title', 'year'])]

Facets / SearchPanes (Doctrine)

SearchPanes filtering uses facet_filter[] (e.g. school,in,Lincoln|Roosevelt). Add a filter that understands that parameter.

If you use Survos\MeiliBundle\Api\Filter\FacetsFieldSearchFilter in your project, it works fine for Doctrine too.

To generate facet counts for Doctrine SearchPanes, your repository should support counts:

use Survos\CoreBundle\Traits\QueryBuilderHelperInterface;
use Survos\CoreBundle\Traits\QueryBuilderHelperTrait;

class VideoRepository extends ServiceEntityRepository implements QueryBuilderHelperInterface
{
    use QueryBuilderHelperTrait;
}

Implementation Notes

See docs/implementation.md for how the Twig component, Stimulus controller, API Platform normalizer, and pagination extension fit together.

Dev only

composer config repositories.survos_grid_bundle '{"type": "vcs", "url": "git@github.com:survos/SurvosApiGridBundle.git"}'