PHPackages                             owl-concept/table-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. owl-concept/table-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

owl-concept/table-bundle
========================

Bundle Symfony pour la génération dynamique de tableaux HTML avec tri, filtres et pagination (mode serveur &amp; client) | Symfony bundle for dynamic HTML table generation with sorting, filtering and pagination (server &amp; client mode)

00PHP

Since Mar 5Pushed 3mo agoCompare

[ Source](https://github.com/Jaecko/owl-table-bundle)[ Packagist](https://packagist.org/packages/owl-concept/table-bundle)[ RSS](/packages/owl-concept-table-bundle/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependenciesVersions (1)Used By (0)

OwlTableBundle
==============

[](#owltablebundle)

[![PHP](https://camo.githubusercontent.com/88b464e5614cf654f181925115d47b523dc429fcfe41d59565e42e757f306f29/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253345253344382e312d3838393242462e737667)](https://www.php.net/)[![Symfony](https://camo.githubusercontent.com/b4b1393dc9e6ac9410cecb1dbdaed9eff36a00e3886287ddcbde27d1b00d438a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53796d666f6e792d362e34253230253743253230372e782d3030303030302e737667)](https://symfony.com/)

**\[FR\]** Bundle Symfony pour la génération dynamique de tableaux HTML avec tri, filtres et pagination. **\[EN\]** Symfony bundle for dynamic HTML table generation with sorting, filtering and pagination.

---

Sommaire / Table of Contents
----------------------------

[](#sommaire--table-of-contents)

- [Français](#-fran%C3%A7ais)
    - [Fonctionnalités](#fonctionnalit%C3%A9s)
    - [Installation](#installation)
    - [Configuration](#configuration)
    - [Utilisation](#utilisation)
    - [Types de filtres](#types-de-filtres)
    - [Mode serveur vs client](#mode-serveur-vs-mode-client)
- [English](#-english)
    - [Features](#features)
    - [Installation](#installation-1)
    - [Configuration](#configuration-1)
    - [Usage](#usage)
    - [Filter types](#filter-types)
    - [Server vs client mode](#server-vs-client-mode)

---

🇫🇷 Français
-----------

[](#-français)

### Fonctionnalités

[](#fonctionnalités)

- **Colonnes auto-détectées** — les colonnes sont générées automatiquement à partir des clés de vos données
- **Génération dynamique de tableaux** via un composant Twig réutilisable
- **Tri des colonnes** — côté serveur (rechargement de page) ou côté client (JavaScript)
- **Filtres configurables** — texte libre, liste déroulante, plage de dates
- **Filtres séparés** — template indépendant, plaçable n'importe où dans la page
- **Pagination intégrée** — avec navigation et ellipsis
- **CSS par défaut** — nommage BEM, responsive mobile (les lignes se transforment en cartes)
- **JavaScript vanilla** — zéro dépendance, auto-initialisation
- **API fluide** — style Builder inspiré du FormBuilder de Symfony

### Installation

[](#installation)

Ajoutez le repository et le package dans votre `composer.json` :

```
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/Jaecko/owl-table-bundle"
        }
    ],
    "require": {
        "owl-concept/table-bundle": "dev-main"
    }
}
```

Puis lancez :

```
composer update
```

Enregistrez le bundle dans `config/bundles.php` :

```
return [
    // ...
    OwlConcept\TableBundle\OwlTableBundle::class => ['all' => true],
];
```

Installez les assets (CSS &amp; JS) :

```
php bin/console assets:install
```

Incluez le CSS et le JS dans votre template de base :

```

```

### Configuration

[](#configuration)

Configuration optionnelle dans `config/packages/owl_table.yaml` :

```
owl_table:
    default_mode: server    # 'server' ou 'client' (défaut: server)
    default_per_page: 20    # Éléments par page (défaut: 20)
    css_class_prefix: owl-table  # Préfixe CSS (défaut: owl-table)
```

### Utilisation

[](#utilisation)

#### Exemple minimal — tableau automatique

[](#exemple-minimal--tableau-automatique)

Passez simplement vos données, le tableau se construit tout seul :

```
use OwlConcept\TableBundle\Builder\TableBuilder;

#[Route('/users', name: 'users_list')]
public function list(TableBuilder $tableBuilder): Response
{
    $users = [
        ['name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'Admin'],
        ['name' => 'Bob', 'email' => 'bob@example.com', 'role' => 'User'],
    ];

    $table = $tableBuilder->create('users_table')
        ->setData($users)  // Les colonnes "name", "email", "role" sont auto-détectées
        ->build();

    return $this->render('user/list.html.twig', ['table' => $table]);
}
```

> Les en-têtes sont générés automatiquement : `created_at` → **Created at**, `firstName` → **First name**, `email` → **Email**

#### Renommer les colonnes avec `setLabels()`

[](#renommer-les-colonnes-avec-setlabels)

Deux syntaxes possibles :

```
// Mode associatif — par clé
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['name' => 'Nom', 'email' => 'Courriel', 'role' => 'Rôle', 'created_at' => 'Créé le'])
    ->build();

// Mode indexé — dans l'ordre des colonnes détectées
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['Nom', 'Courriel', 'Rôle', 'Créé le'])
    ->build();
```

> En mode indexé, les labels sont appliqués dans l'ordre des clés détectées. Vous pouvez ne renommer que les premières colonnes : `->setLabels(['Nom', 'Courriel'])` ne renomme que les 2 premières.

#### Configurer par tableaux (rapide)

[](#configurer-par-tableaux-rapide)

Chaque aspect est configurable via sa propre méthode, sans toucher à `configureColumn()` :

```
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['Nom', 'Email', 'Rôle', 'Créé le'])
    ->setSortable(['name', 'email', 'created_at'])
    ->setFilterable(['name' => 'text', 'role' => 'select', 'created_at' => 'date_range'])
    ->setFilterOptions(['role' => ['Admin', 'User', 'Editor']])
    ->setFormatters(['created_at' => fn($v) => date('d/m/Y', strtotime($v))])
    ->setMode('server')
    ->handleRequest($request)
    ->build();
```

MéthodeMode indexéMode associatif`setLabels()``['Nom', 'Email']` (dans l'ordre)`['name' => 'Nom']` (par clé)`setSortable()``['name', 'email']` (ces clés deviennent triables)—`setFilterable()``['name', 'role']` (type `text` par défaut)`['name' => 'text', 'role' => 'select']``setFilterOptions()`—`['role' => ['Admin', 'User']]``setFormatters()`—`['price' => fn($v) => ...]``setCssClasses()``['bold', '', 'right']` (dans l'ordre)`['name' => 'bold']``setHeaderClass()`—Classe globale sur tous les `` : `'bg-dark text-white'``setHeaderClasses()``['w-50', 'w-25', '', 'w-25']` (dans l'ordre)`['name' => 'w-50']`#### Configurer colonne par colonne (avancé)

[](#configurer-colonne-par-colonne-avancé)

`configureColumn()` reste disponible pour tout regrouper sur une colonne :

```
->configureColumn('role', [
    'label' => 'Rôle',
    'filterable' => true,
    'filter_type' => 'select',
    'filter_options' => ['Admin', 'User'],
    'css_class' => 'text-center',
])
```

> **Priorité** : `configureColumn()` &gt; `setLabels()` / `setSortable()` / etc. &gt; auto-détection

#### Colonnes avec des données hétérogènes

[](#colonnes-avec-des-données-hétérogènes)

Si certaines lignes ont des clés que d'autres n'ont pas, le tableau détecte l'union de toutes les clés :

```
$data = [
    ['name' => 'Alice', 'email' => 'alice@example.com'],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'phone' => '06 12 34 56 78'],
];

// Résultat : 3 colonnes → Name, Email, Phone
// Alice aura une cellule vide pour "Phone"
$table = $tableBuilder->create('contacts')->setData($data)->build();
```

#### Dans le template Twig

[](#dans-le-template-twig)

```
{% extends 'base.html.twig' %}

{% block body %}
    Utilisateurs

    {# Les filtres peuvent être placés n'importe où : sidebar, en-tête, modal... #}

        {% include '@OwlTable/filters.html.twig' with { table: table } %}

    {# Le tableau avec pagination incluse automatiquement #}
    {% include '@OwlTable/table.html.twig' with { table: table } %}
{% endblock %}
```

#### Avec des entités Doctrine

[](#avec-des-entités-doctrine)

Le builder supporte directement les objets (via les getters et propriétés publiques) :

```
$users = $userRepository->findAll();

// Les colonnes sont détectées via getName(), getEmail(), getRole(), etc.
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setSortable(['name', 'email'])
    ->build();
```

#### Formateurs personnalisés

[](#formateurs-personnalisés)

```
->setFormatters([
    'price' => fn($v) => number_format($v, 2, ',', ' ') . ' €',
    'active' => fn($v) => $v ? 'Oui' : 'Non',
    'created_at' => fn($v) => $v instanceof \DateTimeInterface ? $v->format('d/m/Y') : $v,
])
```

### Types de filtres

[](#types-de-filtres)

TypeDescriptionParamètres`text`Champ texte libre avec recherche partielle (insensible à la casse)—`select`Liste déroulante`filter_options` : tableau des valeurs possibles`date_range`Deux champs date (du / au)—### Mode serveur vs mode client

[](#mode-serveur-vs-mode-client)

Mode serveurMode client**Tri**Liens `` avec paramètres URL, rechargement de pageBoutons ``, tri instantané en JS**Filtres**Formulaire ``, soumission classiqueÉcoute des événements `input`/`change`, filtrage instantané**Pagination**Liens `` avec `?page=N`Boutons ``, changement de page sans rechargement**Données**Seule la page courante est dans le HTMLToutes les données sont embarquées en JSON dans un attribut `data-*`**Cas d'usage**Grands jeux de données, requêtes DB paginéesPetits jeux de données (&lt; 500 lignes)#### Requêtes DB avec le mode serveur

[](#requêtes-db-avec-le-mode-serveur)

En mode serveur avec une base de données, utilisez les accesseurs du builder pour construire vos requêtes :

```
$table = $tableBuilder->create('users_table')
    ->setData([]) // données vides pour l'instant
    ->setSortable(['name', 'email'])
    ->setFilterable(['name' => 'text'])
    ->setMode('server')
    ->handleRequest($request);

// Récupérer les valeurs parsées pour construire la requête DB
$sortField = $tableBuilder->getParsedSortField();       // ex: 'name'
$sortDir = $tableBuilder->getParsedSortDirection();      // ex: 'asc'
$filters = $tableBuilder->getParsedFilters();            // ex: ['name' => 'alice']
$page = $tableBuilder->getParsedPage();                  // ex: 2
$perPage = $tableBuilder->getParsedPerPage();            // ex: 20

// Requête Doctrine avec ces paramètres
$qb = $repo->createQueryBuilder('u');
// ... appliquer les filtres et le tri ...
$total = count($qb->getQuery()->getResult());
$users = $qb->setFirstResult(($page - 1) * $perPage)->setMaxResults($perPage)->getQuery()->getResult();

$table = $tableBuilder
    ->setData($users)
    ->setPagination(page: $page, perPage: $perPage, total: $total)
    ->build();
```

---

🇬🇧 English
----------

[](#-english)

### Features

[](#features)

- **Auto-detected columns** — columns are automatically generated from your data keys
- **Dynamic table generation** via a reusable Twig component
- **Column sorting** — server-side (page reload) or client-side (JavaScript)
- **Configurable filters** — free text, dropdown select, date range
- **Separate filter template** — independent include, placeable anywhere on the page
- **Built-in pagination** — with navigation and ellipsis
- **Default CSS** — BEM naming, responsive mobile (rows become stacked cards)
- **Vanilla JavaScript** — zero dependencies, auto-initialization
- **Fluent API** — Builder pattern inspired by Symfony's FormBuilder

### Installation

[](#installation-1)

Add the repository and package to your `composer.json`:

```
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/Jaecko/owl-table-bundle"
        }
    ],
    "require": {
        "owl-concept/table-bundle": "dev-main"
    }
}
```

Then run:

```
composer update
```

Register the bundle in `config/bundles.php`:

```
return [
    // ...
    OwlConcept\TableBundle\OwlTableBundle::class => ['all' => true],
];
```

Install the assets (CSS &amp; JS):

```
php bin/console assets:install
```

Include the CSS and JS in your base template:

```

```

### Configuration

[](#configuration-1)

Optional configuration in `config/packages/owl_table.yaml`:

```
owl_table:
    default_mode: server    # 'server' or 'client' (default: server)
    default_per_page: 20    # Items per page (default: 20)
    css_class_prefix: owl-table  # CSS prefix (default: owl-table)
```

### Usage

[](#usage)

#### Minimal example — automatic table

[](#minimal-example--automatic-table)

Just pass your data, the table builds itself:

```
use OwlConcept\TableBundle\Builder\TableBuilder;

#[Route('/users', name: 'users_list')]
public function list(TableBuilder $tableBuilder): Response
{
    $users = [
        ['name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'Admin'],
        ['name' => 'Bob', 'email' => 'bob@example.com', 'role' => 'User'],
    ];

    $table = $tableBuilder->create('users_table')
        ->setData($users)  // Columns "name", "email", "role" are auto-detected
        ->build();

    return $this->render('user/list.html.twig', ['table' => $table]);
}
```

> Headers are generated automatically: `created_at` → **Created at**, `firstName` → **First name**, `email` → **Email**

#### Rename columns with `setLabels()`

[](#rename-columns-with-setlabels)

Two syntaxes available:

```
// Associative mode — by key
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['name' => 'Full Name', 'email' => 'Email Address', 'created_at' => 'Joined'])
    ->build();

// Indexed mode — in order of detected columns
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['Full Name', 'Email Address', 'Role', 'Joined'])
    ->build();
```

> In indexed mode, labels are applied in the order of detected keys. You can rename only the first columns: `->setLabels(['Full Name', 'Email Address'])` only renames the first 2.

#### Configure with arrays (quick)

[](#configure-with-arrays-quick)

Each aspect has its own method, no need for `configureColumn()`:

```
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setLabels(['Full Name', 'Email', 'Role', 'Joined'])
    ->setSortable(['name', 'email', 'created_at'])
    ->setFilterable(['name' => 'text', 'role' => 'select', 'created_at' => 'date_range'])
    ->setFilterOptions(['role' => ['Admin', 'User', 'Editor']])
    ->setFormatters(['created_at' => fn($v) => date('M d, Y', strtotime($v))])
    ->setMode('server')
    ->handleRequest($request)
    ->build();
```

MethodIndexed modeAssociative mode`setLabels()``['Name', 'Email']` (in order)`['name' => 'Name']` (by key)`setSortable()``['name', 'email']` (these keys become sortable)—`setFilterable()``['name', 'role']` (default `text` type)`['name' => 'text', 'role' => 'select']``setFilterOptions()`—`['role' => ['Admin', 'User']]``setFormatters()`—`['price' => fn($v) => ...]``setCssClasses()``['bold', '', 'right']` (in order)`['name' => 'bold']``setHeaderClass()`—Global class on all ``: `'bg-dark text-white'``setHeaderClasses()``['w-50', 'w-25', '', 'w-25']` (in order)`['name' => 'w-50']`#### Configure column by column (advanced)

[](#configure-column-by-column-advanced)

`configureColumn()` is still available to group all options on a single column:

```
->configureColumn('role', [
    'label' => 'Role',
    'filterable' => true,
    'filter_type' => 'select',
    'filter_options' => ['Admin', 'User'],
    'css_class' => 'text-center',
])
```

> **Priority**: `configureColumn()` &gt; `setLabels()` / `setSortable()` / etc. &gt; auto-detection

#### Heterogeneous data

[](#heterogeneous-data)

If some rows have keys that others don't, the table detects the union of all keys:

```
$data = [
    ['name' => 'Alice', 'email' => 'alice@example.com'],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'phone' => '+33 6 12 34 56 78'],
];

// Result: 3 columns → Name, Email, Phone
// Alice will have an empty cell for "Phone"
$table = $tableBuilder->create('contacts')->setData($data)->build();
```

#### In the Twig template

[](#in-the-twig-template)

```
{% extends 'base.html.twig' %}

{% block body %}
    Users

    {# Filters can be placed anywhere: sidebar, header, modal... #}

        {% include '@OwlTable/filters.html.twig' with { table: table } %}

    {# Table with pagination automatically included #}
    {% include '@OwlTable/table.html.twig' with { table: table } %}
{% endblock %}
```

#### With Doctrine entities

[](#with-doctrine-entities)

The builder supports objects directly (via getters and public properties):

```
$users = $userRepository->findAll();

// Columns are detected via getName(), getEmail(), getRole(), etc.
$table = $tableBuilder->create('users_table')
    ->setData($users)
    ->setSortable(['name', 'email'])
    ->build();
```

#### Custom formatters

[](#custom-formatters)

```
->setFormatters([
    'price' => fn($v) => '$' . number_format($v, 2),
    'active' => fn($v) => $v ? 'Yes' : 'No',
    'created_at' => fn($v) => $v instanceof \DateTimeInterface ? $v->format('M d, Y') : $v,
])
```

### Filter types

[](#filter-types)

TypeDescriptionParameters`text`Free text field with partial search (case-insensitive)—`select`Dropdown select`filter_options`: array of possible values`date_range`Two date fields (from / to)—### Server vs client mode

[](#server-vs-client-mode)

Server modeClient mode**Sorting**`` links with URL parameters, page reload`` elements, instant JS sorting**Filters**``, classic submissionListens to `input`/`change` events, instant filtering**Pagination**`` links with `?page=N``` elements, page change without reload**Data**Only the current page is in the HTMLFull dataset embedded as JSON in a `data-*` attribute**Use case**Large datasets, paginated DB queriesSmall datasets (&lt; 500 rows)#### DB queries with server mode

[](#db-queries-with-server-mode)

In server mode with a database, use the builder's accessors to build your queries:

```
$table = $tableBuilder->create('users_table')
    ->setData([]) // empty data for now
    ->setSortable(['name', 'email'])
    ->setFilterable(['name' => 'text'])
    ->setMode('server')
    ->handleRequest($request);

// Get parsed values to build your DB query
$sortField = $tableBuilder->getParsedSortField();       // e.g. 'name'
$sortDir = $tableBuilder->getParsedSortDirection();      // e.g. 'asc'
$filters = $tableBuilder->getParsedFilters();            // e.g. ['name' => 'alice']
$page = $tableBuilder->getParsedPage();                  // e.g. 2
$perPage = $tableBuilder->getParsedPerPage();            // e.g. 20

// Doctrine query using these parameters
$qb = $repo->createQueryBuilder('u');
// ... apply filters and sorting ...
$total = count($qb->getQuery()->getResult());
$users = $qb->setFirstResult(($page - 1) * $perPage)->setMaxResults($perPage)->getQuery()->getResult();

$table = $tableBuilder
    ->setData($users)
    ->setPagination(page: $page, perPage: $perPage, total: $total)
    ->build();
```

---

License
-------

[](#license)

Proprietary — Owl Concept

###  Health Score

17

—

LowBetter than 6% of packages

Maintenance53

Moderate activity, may be stable

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/d9c48f4a438b587779711c9b1619fc238d635817cadc99091820d553700abcca?d=identicon)[Jaecko](/maintainers/Jaecko)

### Embed Badge

![Health badge](/badges/owl-concept-table-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/owl-concept-table-bundle/health.svg)](https://phpackages.com/packages/owl-concept-table-bundle)
```

###  Alternatives

[digitoimistodude/dude-most-read-posts

A developer-friendly plugin to count most read posts

162.5k](/packages/digitoimistodude-dude-most-read-posts)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
