kevinpirnie/kpt-datatables

Advanced PHP DataTables library with CRUD operations, search, sorting, pagination, bulk actions, and multi-framework theme support (UIKit3, Bootstrap 5, Tailwind, Plain)

Maintainers

Package info

github.com/kpirnie/kp-datatables

pkg:composer/kevinpirnie/kpt-datatables

Fund package maintenance!

Paypal

Ko-Fi

Statistics

Installs: 64

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.3.22 2026-04-27 19:42 UTC

This package is auto-updated.

Last update: 2026-04-27 19:44:59 UTC


README

GitHub Issues Last Commit License: MIT

PHP Discord Kevin Pirnie

Advanced PHP DataTables library with full CRUD operations, multi-table JOIN support, per-column filter accordion, calculated columns, footer aggregations, inline editing, Select2 AJAX dropdowns, file uploads, bulk actions, tabbed modal forms, and a theme system covering UIKit3, Bootstrap 5, Tailwind CSS, and a framework-agnostic Plain theme. All rendering is server-side PHP with client-side interactivity handled by a zero-dependency vanilla JS class.

Table of Contents

Requirements

  • PHP 8.2 or higher
  • PDO extension
  • JSON extension
  • MySQL / MariaDB database

Installation

composer require kevinpirnie/kpt-datatables

Dependencies

Package Purpose
kevinpirnie/kpt-database PDO database wrapper with fluent query builder
kevinpirnie/kpt-logger Internal debug/error logging

Quick Start

<?php
require 'vendor/autoload.php';

use KPT\DataTables;

// Database connection configuration
$dbConfig = [
    'server'    => 'localhost',
    'schema'    => 'my_database',
    'username'  => 'db_user',
    'password'  => 'db_pass',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
];

$dt = new DataTables($dbConfig);

// Handle all AJAX requests before any HTML output
if (isset($_POST['action']) || isset($_GET['action'])) {
    $dt->table('users')
       ->handleAjax();
}

// Render CSS and JS assets in your <head> / before </body>
echo DataTables::getCssIncludes('uikit', true, true);
echo DataTables::getJsIncludes('uikit', true, true);

// Render the table
echo $dt
    ->theme('uikit')
    ->table('users')
    ->columns([
        'id'         => 'ID',
        'name'       => 'Full Name',
        'email'      => 'Email',
        'created_at' => 'Created',
    ])
    ->sortable(['name', 'email', 'created_at'])
    ->renderDataTableComponent();

Asset Inclusion

Assets must be included before the rendered table HTML. The static helper methods handle framework CDN links, theme-specific CSS, and all library JS in one call.

// In <head>
echo DataTables::getCssIncludes(string $theme, bool $includeCdn, bool $useMinified);

// Before </body>
echo DataTables::getJsIncludes(string $theme, bool $includeCdn, bool $useMinified);
Parameter Type Description
$theme string 'uikit', 'bootstrap', 'tailwind', 'plain'
$includeCdn bool Include framework assets from CDN
$useMinified bool Use minified CSS/JS versions

UIKit example:

echo DataTables::getCssIncludes('uikit', true, true);
// Outputs: UIKit CDN CSS + /vendor/.../uikit.min.css

echo DataTables::getJsIncludes('uikit', true, true);
// Outputs: UIKit CDN JS + UIKit Icons JS + kpt-datatables.min.js

Bootstrap example:

echo DataTables::getCssIncludes('bootstrap', true, true);
// Outputs: Bootstrap CDN CSS + Bootstrap Icons CDN CSS + /vendor/.../bootstrap.min.css

echo DataTables::getJsIncludes('bootstrap', true, true);
// Outputs: Bootstrap Bundle CDN JS + kpt-datatables.min.js

Tailwind / Plain (no CDN):

echo DataTables::getCssIncludes('tailwind', false, true);
echo DataTables::getJsIncludes('tailwind', false, true);

Tailwind CSS must be compiled separately. See Building Assets.

AJAX Handling

All CRUD operations, search, pagination, sorting, filtering, and bulk actions are handled server-side through a single AJAX endpoint — the same URL that renders the page. The AJAX handler must be invoked before any HTML output.

$dt = new DataTables($dbConfig);

// Call handleAjax() with the same chain you use for rendering
if (isset($_POST['action']) || isset($_GET['action'])) {
    $dt->theme('bootstrap')
       ->table('orders o')
       ->primaryKey('o.id')
       ->join('LEFT', 'customers c', 'o.customer_id = c.id')
       ->columns([...])
       ->addForm('Add Order', [...])
       ->editForm('Edit Order', [...])
       ->handleAjax();
}

// Then render below
echo $dt->renderDataTableComponent();

The handleAjax() method internally routes the action parameter to the appropriate handler and outputs JSON before calling exit. Supported actions dispatched automatically by the JS layer:

Action Trigger
fetch_data Page load, sort, search, filter, pagination
fetch_record Edit button click (loads record into modal)
add_record Add form submit
edit_record Edit form submit
delete_record Delete confirmation
bulk_action Bulk action execute button
inline_edit Inline field save
upload_file Standalone file upload during inline image edit
fetch_aggregations After each data load when footer aggregations are configured
fetch_select2_options Select2 dropdown search
action_callback Custom row action with PHP callback

Themes

The theme is set via the fluent theme() method. It configures all CSS class mappings used throughout rendering and controls which CDN assets are included.

$dt->theme(string $theme, bool $includeCdn = true)
Theme Description
'uikit' UIKit 3 (default)
'bootstrap' Bootstrap 5
'tailwind' Tailwind CSS (requires compilation)
'plain' Framework-agnostic, kp-dt-* classes only
// UIKit with CDN
$dt->theme('uikit');

// Bootstrap without CDN (you load Bootstrap yourself)
$dt->theme('bootstrap', false);

// Tailwind (CDN not applicable)
$dt->theme('tailwind', false);

// Plain
$dt->theme('plain');

Core Configuration Methods

All methods return $this for fluent chaining unless otherwise noted.

table()

Sets the primary database table. Supports table aliases. Auto-loads the table schema from the database for field type detection and form generation.

->table(string $tableName)
// Simple
->table('users')

// With alias (required when using JOIN)
->table('users u')
->table('kptv_stream_other s')

When an alias is used, the base table name (without alias) is automatically tracked separately for INSERT, UPDATE, and DELETE operations.

primaryKey()

Overrides the auto-detected primary key. Supports qualified (aliased) names.

->primaryKey(string $column)
->primaryKey('id')
->primaryKey('u.user_id')
->primaryKey('s.id')

The library automatically strips the table prefix when building WHERE id = ? clauses for mutations.

database()

Configures or replaces the database connection after construction.

->database(array $config)
->database([
    'server'    => 'localhost',
    'schema'    => 'my_db',
    'username'  => 'root',
    'password'  => 'secret',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
])

columns()

Defines which columns to display. Keys are database column names (supports dot notation for joined tables and expression AS alias syntax). Values are display labels or full configuration arrays.

->columns(array $columns)

Simple format:

->columns([
    'id'         => 'ID',
    'u.name'     => 'Full Name',
    'u.email'    => 'Email',
    'r.role_name' => 'Role',
])

Enhanced format (type overrides, options, form classes):

->columns([
    'id'       => 'ID',
    'u.name'   => 'Full Name',
    'u.email'  => ['label' => 'Email Address', 'type' => 'email'],
    'status'   => [
        'label'   => 'Status',
        'type'    => 'boolean',
    ],
    'category' => [
        'label'   => 'Category',
        'type'    => 'select',
        'options' => ['1' => 'News', '2' => 'Blog', '3' => 'Event'],
    ],
    'user_id'  => [
        'label'           => 'Assigned User',
        'type'            => 'select2',
        'query'           => 'SELECT id AS ID, u_name AS Label FROM users',
        'placeholder'     => 'Search users...',
        'min_search_chars' => 2,
        'max_results'     => 50,
    ],
    'created_at' => [
        'label'     => 'Created',
        'type'      => 'datepicker',
        'formatter' => 'MM/DD/YYYY',
    ],
])

See Column Definition Reference for all supported type overrides.

join()

Adds a SQL JOIN clause. Multiple joins are supported and applied to all data, count, and aggregation queries.

->join(string $type, string $table, string $condition)
Parameter Description
$type 'LEFT', 'RIGHT', 'INNER', 'FULL OUTER'
$table Table name, optionally with alias (e.g., 'users u')
$condition Raw ON condition (e.g., 'o.user_id = u.id')
->table('orders o')
->join('LEFT', 'customers c', 'o.customer_id = c.id')
->join('LEFT', 'products p', 'o.product_id = p.id')
->join('INNER', 'order_status s', 'o.status_id = s.id')

where()

Adds static server-side WHERE conditions that always apply to data queries, count queries, and mutations. These are invisible to the user.

->where(array $conditions)

Each condition requires field, comparison, and value keys.

->where([
    ['field' => 'status',     'comparison' => '=',  'value' => 'active'],
    ['field' => 'deleted_at', 'comparison' => '=',  'value' => null],
    ['field' => 'created_at', 'comparison' => '>=', 'value' => '2024-01-01'],
])

Supported comparison operators: =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE, IN, NOT IN, REGEXP

For IN / NOT IN pass an array as the value:

->where([
    ['field' => 'role_id', 'comparison' => 'IN', 'value' => [1, 2, 3]],
])

WHERE conditions applied through where() are also appended to UPDATE and DELETE queries for security, ensuring mutations cannot affect records outside the configured scope.

filter()

Configures the user-facing collapsible filter accordion rendered above the table. Each key is a database column (dot notation supported). Values can be a shorthand operator string or a full configuration array.

->filter(array $filters)

Shorthand:

->filter([
    'name'       => 'LIKE',
    'status'     => '=',
    'created_at' => 'BETWEEN',
])

Full configuration:

->filter([
    'o.status' => [
        'operator'    => '=',
        'label'       => 'Order Status',
        'type'        => 'select',
        'options'     => ['pending' => 'Pending', 'shipped' => 'Shipped', 'delivered' => 'Delivered'],
        'placeholder' => '',
    ],
    'c.name' => [
        'operator'    => 'LIKE',
        'label'       => 'Customer Name',
        'placeholder' => 'Search by name...',
    ],
    'is_active' => [
        'operator' => '=',
        'label'    => 'Active',
        'type'     => 'boolean',
    ],
    'created_at' => [
        'operator' => 'BETWEEN',
        'label'    => 'Date Range',
        'type'     => 'date',
    ],
])

See Filter Accordion Reference for all supported operators and input types.

sortable()

Defines which columns produce clickable sort headers. Pass column names exactly as they appear in columns() keys, or use alias names for aliased expressions.

->sortable(array $columns)
->sortable(['name', 'email', 'created_at'])

// With joined/aliased columns
->sortable(['u.name', 'u.email', 'r.role_name', 'd.dept_name'])

Clicking a sortable header cycles ASC → DESC → ASC. Sort icons update accordingly.

inlineEditable()

Marks specific columns as double-click-to-edit. The appropriate inline editor (text input, select, boolean toggle, image uploader, etc.) is determined from the column's type in the schema or columns() override.

->inlineEditable(array $columns)
->inlineEditable(['name', 'email', 'status'])

// Qualified names
->inlineEditable(['u.name', 'u.email', 'u.status'])

Supported inline edit types: text, email, number, date, datetime-local, textarea, select, select2, boolean, image, datepicker.

perPage()

Sets the initial (default) number of records displayed per page.

->perPage(int $count)
->perPage(25)   // default
->perPage(50)
->perPage(100)

pageSizeOptions()

Configures the options available in the per-page selector. When $includeAll is true, an "All records" option (value 0) is appended.

->pageSizeOptions(array $options, bool $includeAll = true)
->pageSizeOptions([10, 25, 50, 100], true)
->pageSizeOptions([25, 50, 100, 250], false)

The per-page selector renders as a <select> dropdown by default, or as a button group when renderPageSizeSelectorComponent(true) is called.

search()

Enables or disables the global search input. Enabled by default.

->search(bool $enabled = true)
->search(true)   // default
->search(false)  // hide search completely

defaultSort()

Sets the column and direction used for initial data load. Without this, data loads in database natural order.

->defaultSort(string $column, string $direction = 'ASC')
->defaultSort('created_at', 'DESC')
->defaultSort('u.name', 'ASC')

groupBy()

Adds a GROUP BY clause to data and count queries. When a group-by is active, the count query wraps the grouped result in a subquery to return the true number of groups. Aggregation queries also wrap accordingly.

->groupBy(string $column)
->groupBy('user_id')
->groupBy('o.status')

Action Configuration

actions()

Configures the built-in action column (edit / delete buttons) with placement and visibility.

->actions(string $position = 'end', bool $showEdit = true, bool $showDelete = true, array $customActions = [])
->actions('end', true, true)    // Both buttons at end (default)
->actions('start', true, false) // Edit only at start
->actions('end', false, true)   // Delete only at end

actionGroups()

Replaces the default edit/delete buttons with a flexible group system that supports built-in actions, custom link actions, PHP callback actions, and raw HTML injection — all with configurable ordering and separators.

->actionGroups(array $groups)

Each element of $groups is either:

  • An array of built-in action keys ('edit', 'delete')
  • An associative array of custom action configurations

Built-in actions:

->actionGroups([
    ['edit', 'delete'],
])

Custom link action:

->actionGroups([
    [
        'view' => [
            'icon'  => 'search',
            'title' => 'View Record',
            'class' => 'btn-view',
            'href'  => '/records/{id}',
        ],
    ],
    ['edit', 'delete'],
])

Placeholder substitution{id} and {column_name} placeholders in href, title, onclick, and attributes values are replaced with the row's actual data:

->actionGroups([
    [
        'export' => [
            'icon'       => 'download',
            'title'      => 'Export {name}',
            'href'       => '/export/{id}?ref={order_ref}',
            'class'      => 'btn-export',
            'attributes' => [
                'data-id'  => '{id}',
                'data-ref' => '{order_ref}',
            ],
        ],
    ],
    ['edit', 'delete'],
])

PHP callback action (server-side execution):

->actionGroups([
    [
        'approve' => [
            'icon'            => 'check',
            'title'           => 'Approve',
            'class'           => 'btn-approve',
            'confirm'         => 'Approve this record?',
            'success_message' => 'Record approved',
            'error_message'   => 'Approval failed',
            'callback'        => function($rowId, $rowData, $db, $table) {
                return $db->query("UPDATE `{$table}` SET status = 'approved' WHERE id = ?")
                          ->bind([$rowId])
                          ->execute();
            },
        ],
    ],
    ['edit', 'delete'],
])

HTML injection — inject arbitrary HTML before or after any action or group:

->actionGroups([
    [
        'html1' => [
            'location' => 'before',
            'content'  => '<span class="divider">|</span>',
        ],
        'view' => ['icon' => 'search', 'href' => '/view/{id}'],
    ],
    ['edit', 'delete'],
])

Bulk Actions

Enables a checkbox column and a toolbar for performing operations on multiple selected records simultaneously.

->bulkActions(bool $enabled = true, array $actions = [])

With default delete-only:

->bulkActions(true)

With custom actions:

->bulkActions(true, [
    'activate' => [
        'label'           => 'Activate Selected',
        'icon'            => 'check',
        'confirm'         => 'Activate selected records?',
        'success_message' => 'Records activated',
        'error_message'   => 'Activation failed',
        'callback'        => function($ids, $db, $table) {
            $placeholders = implode(',', array_fill(0, count($ids), '?'));
            return $db->query("UPDATE `{$table}` SET active = 1 WHERE id IN ({$placeholders})")
                      ->bind($ids)
                      ->execute();
        },
    ],
    'archive' => [
        'label'    => 'Archive Selected',
        'icon'     => 'folder',
        'confirm'  => 'Archive selected records?',
        'callback' => function($ids, $db, $table) {
            // ...
        },
    ],
])

The callback signature is function(array $ids, Database $db, string $baseTableName): bool|int.

Clicking a row (outside the action/check cell) toggles its checkbox. A "select all" checkbox in the header selects the entire current page.

Modal Forms

addForm()

Configures the "Add Record" modal and its form fields.

->addForm(string $title, array $fields, bool $ajax = true, string $class = '')
->addForm('Add New User', [
    'name' => [
        'type'        => 'text',
        'label'       => 'Full Name',
        'required'    => true,
        'placeholder' => 'Enter full name',
    ],
    'email' => [
        'type'     => 'email',
        'label'    => 'Email Address',
        'required' => true,
    ],
    'role_id' => [
        'type'    => 'select',
        'label'   => 'Role',
        'options' => ['1' => 'Admin', '2' => 'Editor', '3' => 'User'],
    ],
    'status' => [
        'type'  => 'boolean',
        'label' => 'Active',
        'value' => '1',
    ],
])

editForm()

Configures the "Edit Record" modal. The primary key field is automatically injected as a hidden input and populated by the JS fetch_record AJAX call.

->editForm(string $title, array $fields, bool $ajax = true, string $class = '')
->editForm('Edit User', [
    'name'    => ['type' => 'text',    'label' => 'Full Name', 'required' => true],
    'email'   => ['type' => 'email',   'label' => 'Email',     'required' => true],
    'role_id' => [
        'type'    => 'select',
        'label'   => 'Role',
        'options' => ['1' => 'Admin', '2' => 'Editor', '3' => 'User'],
    ],
    'status'  => ['type' => 'boolean', 'label' => 'Active'],
])

Field Types

All field configurations share common keys:

Key Type Description
type string Field type (see table below)
label string Display label
required bool Add required attribute and asterisk
placeholder string Placeholder text
value mixed Default value
default mixed Alias for value
disabled bool Disable the field
class string Extra CSS class on the wrapper
attributes array Additional HTML attributes
options array value => label pairs for select/radio

Supported types:

Type Renders As Notes
text <input type="text"> Default fallback
email <input type="email">
number <input type="number">
url <input type="url">
tel <input type="tel">
password <input type="password">
hidden <input type="hidden"> No label or wrapper rendered
textarea <textarea>
boolean <select> with Active/Inactive Stores 1 / 0
checkbox <input type="checkbox"> Value 1 when checked
radio Radio button group Requires options
select <select> dropdown Requires options
select2 AJAX searchable dropdown Requires query
file <input type="file">
image URL input + file upload + preview
datepicker Styled date picker with format support
static Read-only <p> element Not submitted; renders content key or DB value

select2 field extra keys:

Key Type Default Description
query string SQL query returning ID and Label columns
placeholder string 'Select...' Dropdown placeholder
min_search_chars int 0 Minimum characters before search fires
max_results int 50 Maximum results returned
'user_id' => [
    'type'             => 'select2',
    'label'            => 'Assigned User',
    'query'            => 'SELECT id AS ID, CONCAT(first, " ", last) AS Label FROM users WHERE active = 1',
    'placeholder'      => 'Search users...',
    'min_search_chars' => 2,
    'max_results'      => 25,
    'required'         => true,
]

Query parameter substitution — use {field_name} placeholders in select2 queries to filter options based on other fields in the same record:

'city_id' => [
    'type'  => 'select2',
    'query' => 'SELECT id AS ID, city_name AS Label FROM cities WHERE state_id = {state_id}',
]

datepicker field extra keys:

Key Type Default Description
formatter string 'YYYY-MM-DD' Display format using YYYY, MM, DD tokens
'birth_date' => [
    'type'      => 'datepicker',
    'label'     => 'Date of Birth',
    'formatter' => 'MM/DD/YYYY',
]

Values are stored internally as YYYY-MM-DD regardless of display format.

static field extra keys:

Key Type Description
content string Hardcoded text to display; omit to auto-populate from the DB record
'created_by' => [
    'type'    => 'static',
    'label'   => 'Created By',
    'content' => '', // empty = populated from DB on edit
]

Tabbed Forms

Any form field can be assigned to a named tab by adding a tab key. Fields without a tab key are grouped under a "General" tab that is prepended automatically.

->addForm('Add User', [
    'name'  => ['type' => 'text', 'label' => 'Name'],
    'email' => ['type' => 'email', 'label' => 'Email'],

    'bio'   => ['type' => 'textarea', 'label' => 'Bio',   'tab' => 'Profile'],
    'photo' => ['type' => 'image',    'label' => 'Photo', 'tab' => 'Profile'],

    'role_id'    => ['type' => 'select',  'label' => 'Role',    'tab' => 'Permissions', 'options' => [...]],
    'department' => ['type' => 'select',  'label' => 'Dept',    'tab' => 'Permissions', 'options' => [...]],
])

Tab navigation is rendered using the active theme's tab component (UIKit uk-tab, Bootstrap nav-tabs, or the custom kp-dt-tabs plain/tailwind component).

allow_on Field Overrides

Fields in the edit form can be conditionally modified when a specific condition on the fetched record is met. The server evaluates the condition during fetch_record and sends field overrides back to the client.

'field_name' => [
    'type'     => 'text',
    'label'    => 'Some Field',
    'allow_on' => [
        'field'    => 'status',      // Field from the fetched record to evaluate
        'operator' => '==',          // Comparison operator
        'value'    => 'approved',    // Value to compare against
        'action'   => [
            'set_value'      => 'auto-filled',               // Force value
            'set_attributes' => ['readonly' => 'readonly'],  // Add/remove attributes
            'set_classes'    => ['uk-text-muted'],           // Add CSS classes
        ],
    ],
]

Supported operators for allow_on: ==, !=, >, >=, <, <=, IN, NOT IN

To remove an attribute set null or false as its value in set_attributes.

Calculated Columns

Computed columns are added to the SELECT as SQL expressions and rendered in the table like any other column. They cannot be edited inline.

calculatedColumn()

Builds the expression from an array of column names joined by an operator.

->calculatedColumn(string $alias, string $label, array $columns, string $operator = '+')
Operator
+ Addition
- Subtraction
* Multiplication
/ Division
% Modulus
->calculatedColumn('line_total', 'Line Total', ['quantity', 'unit_price'], '*')
->calculatedColumn('profit',     'Profit',     ['revenue', 'cost'],        '-')

calculatedColumnRaw()

Accepts a raw SQL expression for complex calculations.

->calculatedColumnRaw(string $alias, string $label, string $expression)
->calculatedColumnRaw('margin_pct', 'Margin %',
    '((sell_price - cost_price) / sell_price) * 100')

->calculatedColumnRaw('full_name', 'Full Name',
    'CONCAT(u.first_name, " ", u.last_name)')

Footer Aggregations

Calculates SUM and/or AVG per column and displays the results in the table footer. Two scopes are supported: page (calculated client-side from the current page data) and all (calculated server-side across the full filtered recordset).

footerAggregate()

->footerAggregate(string $column, string $type = 'sum', string $scope = 'both', string $label = '')
Parameter Options
$type 'sum', 'avg', 'both'
$scope 'page', 'all', 'both'
->footerAggregate('amount',   'sum',  'both')
->footerAggregate('tax',      'avg',  'all')
->footerAggregate('quantity', 'both', 'page', 'Page Totals')

footerAggregateColumns()

Applies the same type and scope to multiple columns at once.

->footerAggregateColumns(array $columns, string $type = 'sum', string $scope = 'both', string $label = '')
->footerAggregateColumns(['amount', 'tax', 'shipping'], 'sum', 'both')

Aggregation columns work with calculated columns — pass the alias name:

->calculatedColumn('line_total', 'Line Total', ['quantity', 'unit_price'], '*')
->footerAggregate('line_total', 'sum', 'both')

Styling

tableClass()

Overrides the default CSS classes on the <table> element.

->tableClass(string $class)
->tableClass('uk-table uk-table-striped uk-table-hover my-custom-table')
->tableClass('table table-dark table-sm')

rowClass()

Sets a base CSS class applied to every <tr>. The record's primary key value is appended, e.g., my-row-42.

->rowClass(string $class)
->rowClass('data-row')
// Produces: class="data-row-42 row-select"

columnClasses()

Applies CSS classes to specific <td> (and <th>) elements by column key.

->columnClasses(array $classes)
->columnClasses([
    'id'      => 'uk-table-shrink',
    'u.name'  => 'uk-text-bold',
    'u.email' => 'uk-text-primary',
    'status'  => 'uk-text-center',
    's_stream_uri' => 'txt-truncate',
])

File Uploads

Configures server-side validation for file uploads submitted through file or image form fields, or via inline image editing.

->fileUpload(string $uploadPath = 'uploads/', array $allowedExtensions = [], int $maxFileSize = 10485760)
Parameter Default Description
$uploadPath 'uploads/' Destination directory (created if absent)
$allowedExtensions ['jpg','jpeg','png','gif','pdf','doc','docx'] Whitelist of extensions (without dot)
$maxFileSize 10485760 (10 MB) Maximum size in bytes
->fileUpload('uploads/avatars/', ['jpg', 'jpeg', 'png', 'gif', 'webp'], 5242880)
->fileUpload('uploads/documents/', ['pdf', 'doc', 'docx', 'xls', 'xlsx'])

Uploaded files are stored with a uniqid() prefix to prevent collisions. The stored filename (not path) is saved to the database field.

Rendering

renderDataTableComponent()

Renders the complete DataTable HTML: the container, filter accordion, table (with header, body, footer, aggregation rows), all modals, and the JS initialization script. This is the primary output method.

echo $dt->renderDataTableComponent();

Static Methods

getCssIncludes()

DataTables::getCssIncludes(string $theme = 'uikit', bool $includeCdn = true, bool $useMinified = false): string

getJsIncludes()

DataTables::getJsIncludes(string $theme = 'uikit', bool $includeCdn = true, bool $useMinified = false): string

Standalone Component Renderers

Individual components can be rendered outside renderDataTableComponent() for custom layouts.

// Filter accordion panel (omit from renderDataTableComponent() to avoid duplicates)
echo $dt->renderFilterAccordionComponent();

// Search form input + reset button
echo $dt->renderSearchFormComponent();

// Bulk actions toolbar (add button + bulk operation buttons)
echo $dt->renderBulkActionsComponent();

// Per-page selector as dropdown (default)
echo $dt->renderPageSizeSelectorComponent();

// Per-page selector as button group
echo $dt->renderPageSizeSelectorComponent(true);

// Pagination list + record info text
echo $dt->renderPaginationComponent();

When calling renderFilterAccordionComponent() manually, remove the internal call by ensuring the filter accordion is not rendered inside renderContainer(). Rendering it twice causes duplicate DOM IDs.

Filter Accordion Reference

Operators

Operator SQL Behavior Input Rendered
= Exact match Text input
!= Not equal Text input
>, >=, <, <= Numeric/date comparison Text or number input
LIKE Partial match (%value% auto-wrapped) Text input
NOT LIKE Inverse partial match Text input
IN Comma-separated value list Text input (hint shown)
NOT IN Exclude comma-separated list Text input
BETWEEN Date or number range Two side-by-side inputs with From/To labels
REGEXP Regular expression match Text input

Input Types

Type Renders
text (default) <input type="text">
number <input type="number">
date <input type="date">
datetime-local <input type="datetime-local">
email <input type="email">
boolean <select> with All / Active / Inactive
select <select> — requires options in config

Active Filter Badge

When filters are applied, a count badge appears in the accordion header. The badge updates automatically as filters are added or removed and clears when resetFilters() is called.

WHERE Conditions Reference

where() conditions differ from filter() conditions:

where() filter()
Who sets it Developer End user (via accordion UI)
Persists across requests Always Only while inputs are filled
Applied to mutations Yes (UPDATE, DELETE) No
Visible to user No Yes

Both are applied simultaneously — where() conditions are evaluated first, filter conditions are appended with AND.

Column Definition Reference

When using the enhanced column format (array value instead of string label), the following keys control display and form behavior:

Key Description
label Display label in table header and form
type Override the auto-detected field type for display and inline editing
options value => label map for select type display and inline editing
query SQL query for select2 type (must return ID and Label columns)
min_search_chars Minimum characters before select2 search fires
max_results Maximum select2 results
formatter Date format string for datepicker type
class Extra CSS class on the column's form wrapper
attributes Extra HTML attributes on the column's form input
placeholder Placeholder text

Auto-detected types from MySQL schema:

MySQL Column Type Detected As
tinyint(1), boolean, bit(1) boolean
int, bigint, smallint, etc. number
decimal, float, double number
datetime, timestamp datetime-local
date date
time time
text, longtext, mediumtext textarea
enum select
varchar text (or email if column name contains "email")

JavaScript API

The DataTablesJS class is instantiated automatically by the PHP renderInitScript() output and exposed on window.DataTables. You can call its methods directly from inline event handlers or your own JS.

Core Methods

Method Description
DataTables.loadData() Reload table data with current search/sort/filter/page state
DataTables.goToPage(page) Navigate to a specific page number
DataTables.resetSearch() Clear search input and reload
DataTables.applyFilters() Read filter inputs and reload
DataTables.resetFilters() Clear all filter inputs and reload
DataTables.changePageSize(size, event) Change records per page

CRUD Methods

Method Description
DataTables.showAddModal(event) Open the add record modal
DataTables.showEditModal(id) Fetch record and open edit modal
DataTables.showDeleteModal(id) Open delete confirmation modal
DataTables.confirmDelete() Execute the pending delete
DataTables.submitAddForm(event) Submit the add form via AJAX
DataTables.submitEditForm(event) Submit the edit form via AJAX

Selection and Bulk

Method Description
DataTables.toggleSelectAll(checkbox) Toggle all row checkboxes
DataTables.toggleRowSelection(checkbox) Toggle a single row checkbox
DataTables.executeBulkAction() Execute action from <select> bulk action UI
DataTables.executeBulkActionDirect(action, event) Execute a named bulk action from a button

Notifications and Modals

Method Description
DataTables.showNotification(message, status) Show themed notification ('success', 'danger', 'warning')
DataTables.showModal(modalId) Open a modal by ID
DataTables.hideModal(modalId) Close a modal by ID
DataTables.showConfirm(message) Show themed confirm dialog, returns Promise

Global Helper Objects

Object Purpose
KPDataTablesPlain Modal and notification helpers for plain/tailwind themes
KPDataTablesBootstrap Bootstrap Toast and modal confirm helper
KPDataTablesDatepicker Date formatting and ISO parsing utilities
KPTSelect2 AJAX-powered Select2 class (no jQuery)

Building Assets

Node.js tooling is used to compile and minify JS and CSS. Install dependencies first:

npm install

Available npm Scripts

Command Description
npm run build Compile Tailwind CSS, then minify all JS and CSS
npm run build:js Minify JS bundle only
npm run build:css Minify all theme CSS files only
npm run build:tailwind Compile Tailwind CSS from tailwind.src.css
npm run watch:tailwind Watch and recompile Tailwind CSS on changes
npm run dev:tailwind One-shot Tailwind compile without minification
npm run dev Tailwind compile + minify all

Output Locations

Asset Output
JS bundle src/assets/js/dist/kpt-datatables.min.js
CSS (per theme) src/assets/css/dist/{theme}.min.css
Tailwind source src/assets/css/themes/tailwind.css

Testing

# Run all tests
composer test

# Run with HTML coverage report
composer test-coverage

# Code style check (PSR-12)
composer cs-check

# Code style auto-fix
composer cs-fix

Tests cover UIKit, Bootstrap, Tailwind, and Plain theme rendering as well as AJAX handler routing. The CI matrix runs against PHP 8.2, 8.3, and 8.4 with both lowest and highest dependency sets.

Security

  • All GET/POST inputs are sanitized before use in SQL queries.
  • All SQL values are passed through PDO bound parameters — no string interpolation of user input.
  • Column names, sort directions, and filter operators are validated against whitelists.
  • The where() conditions are appended to all mutations, scoping UPDATE and DELETE to only records matching the developer-defined filter.
  • Allowed AJAX actions are validated against a whitelist before dispatch.
  • Inline editable columns are validated server-side before any UPDATE is executed.
  • File uploads validate extension and size server-side independently of the client.

For security-related issues, email security@kpirnie.com rather than opening a GitHub issue.

Full Configuration Example

<?php
require 'vendor/autoload.php';

use KPT\DataTables;

$dbConfig = [
    'server'    => 'localhost',
    'schema'    => 'my_app',
    'username'  => 'db_user',
    'password'  => 'db_pass',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
];

$dt = new DataTables($dbConfig);

if (isset($_POST['action']) || isset($_GET['action'])) {
    $dt->theme('uikit')
       ->table('orders o')
       ->primaryKey('o.id')
       ->join('LEFT', 'customers c', 'o.customer_id = c.id')
       ->join('LEFT', 'products p',  'o.product_id  = p.id')
       ->where([
           ['field' => 'o.deleted_at', 'comparison' => '=', 'value' => null],
       ])
       ->addForm('New Order', [
           'customer_id' => [
               'type'             => 'select2',
               'label'            => 'Customer',
               'query'            => 'SELECT id AS ID, company_name AS Label FROM customers WHERE active = 1',
               'placeholder'      => 'Search customers...',
               'min_search_chars' => 2,
               'required'         => true,
           ],
           'product_id' => [
               'type'    => 'select',
               'label'   => 'Product',
               'options' => ['1' => 'Widget A', '2' => 'Widget B'],
               'required' => true,
           ],
           'quantity'   => ['type' => 'number', 'label' => 'Quantity', 'required' => true],
           'status'     => ['type' => 'boolean', 'label' => 'Active'],
           'notes'      => ['type' => 'textarea', 'label' => 'Notes', 'tab' => 'Notes'],
           'attachment' => ['type' => 'file',     'label' => 'Attach', 'tab' => 'Notes'],
       ])
       ->editForm('Edit Order', [
           'customer_id' => [
               'type'    => 'select2',
               'label'   => 'Customer',
               'query'   => 'SELECT id AS ID, company_name AS Label FROM customers',
               'required' => true,
           ],
           'quantity'   => ['type' => 'number',   'label' => 'Quantity'],
           'status'     => ['type' => 'boolean',  'label' => 'Active'],
           'notes'      => ['type' => 'textarea', 'label' => 'Notes', 'tab' => 'Notes'],
       ])
       ->handleAjax();
}

echo DataTables::getCssIncludes('uikit', true, true);

echo $dt
    ->theme('uikit')
    ->table('orders o')
    ->primaryKey('o.id')
    ->join('LEFT', 'customers c', 'o.customer_id = c.id')
    ->join('LEFT', 'products p',  'o.product_id  = p.id')
    ->where([
        ['field' => 'o.deleted_at', 'comparison' => '=', 'value' => null],
    ])
    ->columns([
        'o.id'           => 'Order #',
        'c.company_name' => 'Customer',
        'p.product_name' => 'Product',
        'o.quantity'     => 'Qty',
        'o.unit_price'   => 'Unit Price',
        'o.status'       => ['label' => 'Status', 'type' => 'boolean'],
        'o.created_at'   => 'Ordered',
    ])
    ->calculatedColumn('line_total', 'Line Total', ['o.quantity', 'o.unit_price'], '*')
    ->footerAggregate('o.quantity', 'sum', 'page')
    ->footerAggregate('line_total', 'sum', 'both', 'Totals')
    ->sortable(['c.company_name', 'p.product_name', 'o.quantity', 'o.created_at'])
    ->inlineEditable(['o.quantity', 'o.status'])
    ->filter([
        'c.company_name' => ['operator' => 'LIKE',    'label' => 'Customer'],
        'o.status'       => ['operator' => '=',       'label' => 'Status', 'type' => 'boolean'],
        'o.created_at'   => ['operator' => 'BETWEEN', 'label' => 'Order Date', 'type' => 'date'],
    ])
    ->defaultSort('o.created_at', 'DESC')
    ->perPage(25)
    ->pageSizeOptions([25, 50, 100], true)
    ->bulkActions(true, [
        'cancel' => [
            'label'    => 'Cancel Selected',
            'icon'     => 'close',
            'confirm'  => 'Cancel selected orders?',
            'callback' => function($ids, $db, $table) {
                $ph = implode(',', array_fill(0, count($ids), '?'));
                return $db->query("UPDATE `{$table}` SET status = 0 WHERE id IN ({$ph})")
                          ->bind($ids)->execute();
            },
        ],
    ])
    ->actionGroups([
        [
            'invoice' => [
                'icon'  => 'print',
                'title' => 'Print Invoice',
                'href'  => '/invoice/{id}',
                'class' => 'btn-invoice',
            ],
        ],
        ['edit', 'delete'],
    ])
    ->columnClasses([
        'o.id'       => 'uk-table-shrink',
        'o.status'   => 'uk-text-center',
        'line_total' => 'uk-text-right',
    ])
    ->fileUpload('uploads/attachments/', ['pdf', 'doc', 'docx', 'png', 'jpg'], 10485760)
    ->renderDataTableComponent();

echo DataTables::getJsIncludes('uikit', true, true);

Browser Support

Chrome 60+, Firefox 60+, Safari 12+, Edge 79+

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -m 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

Roadmap

  • CSV / Excel / PDF export
  • REST API endpoints
  • Multi-framework theme support
  • Calculated columns and footer aggregations
  • Column filter accordion with BETWEEN date range support
  • Select2 AJAX searchable dropdowns
  • Tabbed modal forms
  • Conditional field overrides (allow_on)
  • Custom datepicker with format tokens

License

The MIT License (MIT). See LICENSE for details.

Support

Made with ❤️ by Kevin Pirnie