Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ A Symfony 8+ bundle for Bootstrap-first business datatables driven by PHP defini
- **Twig Rendering**: Render tables with a single Twig function: `{{ zhortein_datatable() }}`.
- **Ajax Fragments**: Seamless server-side updates using vanilla Stimulus.
- **Data Providers**: Native support for **Doctrine ORM** and **Array** providers.
- **Filtering**: Built-in global search and permanent backend filters.
- **Filtering**: Built-in global search, toolbar/header filters, and advanced **Search Builder**.
- **Actions**: Declarative row and global actions with CSRF-aware non-GET support.
- **Exports**: Server-side CSV and optional XLSX exports.
- **Customization**: Flexible UI/UX customization via Twig blocks and themes.
Expand Down Expand Up @@ -91,6 +91,7 @@ final class UserDatatable implements DatatableInterface
- [Providers Overview](docs/providers.md)
- [Doctrine Provider](docs/doctrine-provider.md)
- [Filters](docs/filters.md)
- [Advanced Filters](docs/advanced-filters.md)
- [Actions & Security](docs/actions.md)
- [Bulk Actions & Selection](docs/bulk-actions.md)
- [Exports](docs/exports.md)
Expand Down
135 changes: 135 additions & 0 deletions docs/advanced-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Advanced Filter Expressions

The "Advanced Filter Expressions" system (also referred to as **Search Builder**) allows users to build complex, nested filtering logic using `AND`/`OR` groups and various operators.

## Enabling Advanced Filters

Advanced filters are disabled by default. You can enable them globally or per-datatable.

### Global Enablement

In your `zhortein_datatable.yaml` configuration:

```yaml
zhortein_datatable:
search_builder_enabled: true
```

### Per-Datatable Enablement

When rendering the datatable in Twig:

```twig
{{ zhortein_datatable('users', {
searchBuilder: true
}) }}
```

## Declaring Filterable Fields

Unlike simple filters, fields for the Search Builder must be explicitly declared in your `DatatableDefinition` using `addAdvancedFilterField()`. This ensures a strict security boundary where only intended fields are exposed to the frontend.

```php
use Zhortein\DatatableBundle\Enum\FilterType;
use Zhortein\DatatableBundle\Enum\FilterOperator;

$definition->addAdvancedFilterField(
name: 'email',
field: 'e.email',
label: 'Email',
type: FilterType::Text,
allowedOperators: [
FilterOperator::Equals,
FilterOperator::Contains,
FilterOperator::StartsWith,
]
);
```

### Options

| Option | Description |
|---|---|
| `name` | Public field name used in the frontend payload. |
| `field` | Provider field targeted (e.g., `e.email` or `organization.name`). |
| `label` | (Optional) Human-readable label rendered in the UI. Defaults to a capitalized version of `name`. |
| `type` | `FilterType` enum value (Text, Choice, Boolean, Date, Number). |
| `allowedOperators` | (Optional) List of `FilterOperator` allowed for this field. If empty, all operators compatible with the type are allowed. |
| `choices` | (Optional) Array of choices for `Choice` fields. |

## Supported Types and Operators

### Types

The Search Builder supports the following types from the `FilterType` enum:
- `Text`
- `Choice`
- `Boolean`
- `Date`
- `Number`

### Operators

The following operators are supported (see `ComparisonOperator` enum for internal values):

| Label | Internal Code | Behavior |
|---|---|---|
| **Equals** | `eq` | Exact match. |
| **Not Equals** | `neq` | Not equal match. |
| **Contains** | `contains` | Case-insensitive `LIKE %value%`. |
| **Does not contain** | `not_contains` | Case-insensitive `NOT LIKE %value%`. |
| **Starts with** | `starts_with` | Case-insensitive `LIKE value%`. |
| **Ends with** | `ends_with` | Case-insensitive `LIKE %value`. |
| **Greater than** | `gt` | `>` comparison. |
| **Greater than or equals** | `gte` | `>=` comparison. |
| **Less than** | `lt` | `<` comparison. |
| **Less than or equals** | `lte` | `<=` comparison. |
| **Between** | `between` | `BETWEEN value1 AND value2`. |
| **Is null** | `is_null` | `IS NULL` check. |
| **Is not null** | `is_not_null` | `IS NOT NULL` check. |
| **In** | `in` | `IN (value1, value2, ...)` check. |
| **Not in** | `not_in` | `NOT IN (value1, value2, ...)` check. |

## Logic and Nesting

The Search Builder supports `AND` and `OR` logic. Users can create nested groups to build complex expressions:

- **Root Group**: The top-level group (defaults to `AND`).
- **Sub-groups**: Additional groups can be added inside other groups (up to a depth of 3).

## Provider Behavior

### Doctrine Provider

Advanced filters are applied directly to the Doctrine `QueryBuilder`.
- **Join Handling**: Joins are automatically managed based on the field references (e.g., `organization.name` will use the `organization` alias).
- **Case Sensitivity**: String comparisons (`Contains`, `Starts with`, etc.) use `LOWER()` on both the field and the parameter for database-agnostic case-insensitivity.
- **Security**: All parameters are bound using Doctrine parameter binding to prevent SQL injection.

### Array Provider

Advanced filters work with the Array provider as well. The evaluator performs in-memory comparisons:
- **Case Sensitivity**: String comparisons are performed using `mb_strtolower`.
- **Date Handling**: Supports `\DateTimeInterface` objects and `Y-m-d` date strings.
- **Type Coercion**: Performs basic type coercion (e.g., numeric strings vs numbers) to ensure consistent results.

## Export Behavior

When exporting to CSV or XLSX, the active advanced filters are automatically applied to the exported dataset, ensuring the export matches the user's current view.

## Security Boundaries

Security is a core design principle of the Advanced Filters system:

1. **Backend-defined Fields**: Only fields explicitly declared with `addAdvancedFilterField()` can be used in expressions. Attempting to filter on undeclared fields will result in the condition being ignored.
2. **No Arbitrary DQL/SQL**: The frontend sends a declarative JSON payload. The backend parses this payload into a structured expression tree. No raw DQL or SQL is ever accepted from the client.
3. **Strict Operators**: The backend validates that only supported operators are used.
4. **Parameter Binding**: All values from the frontend are treated as parameters and bound using Doctrine's secure parameter binding system. No values are ever directly concatenated into query strings.
5. **Depth Limit**: The expression tree depth is limited (default 3) to prevent complex query exhaustion attacks.

## Limitations

- **Saved Presets**: There is currently no support for saving or sharing filter presets.
- **User Persistence**: Advanced filters are not persisted between sessions or page reloads.
- **Third-party Widgets**: The current implementation uses standard Bootstrap inputs; custom widgets like Select2 or specialized datepickers are not yet supported.
- **Collection-valued Associations**: Filtering on collection-valued associations (e.g., "Users having at least one Role with name X") is not supported.
4 changes: 2 additions & 2 deletions docs/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Currently implemented:
- **Types**: Text, Choice, Boolean, Date, Date Range, Number, Number Range.
- **Layouts**: Toolbar (default) and Column Header Dropdowns.
- **Features**: Active filter summary, clear filters action, Stimulus-powered refresh with debouncing.
- **Advanced Filters**: [Advanced search builder](advanced-filters.md) for complex `AND`/`OR` logic.

Not implemented yet:
- Advanced SearchBuilder-style expressions (only `AND` is supported).
- Nested filter groups.
- Nested filter groups (simple filters only).
- Persisted filter presets.
- Custom filter widgets (Select2, datepickers).
- Collection-valued association filters.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This bundle is a Symfony 8+ datatable bundle for Bootstrap-first business tables

- [Providers](providers.md): Array and Doctrine data sources.
- [Filters](filters.md): Toolbar and header-based data filtering.
- [Advanced Filters](advanced-filters.md): Complex nested filtering with Search Builder.
- [Actions and Security](actions.md): Row-level and global table actions with CSRF and authorization.
- [Bulk Actions and Selection](bulk-actions.md): Managing multiple rows at once.
- [UI/UX and Controls](ui-ux.md): Search, pagination, sorting, and UI customization.
Expand Down
Loading