Skip to content
Open
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
</p>
</p>

> [!CAUTION]
> This package is looking for new maintainers or forks with continued development.
> For maintainer role, please contact via e-mail: kontakt@swroblewski.pl
> Links to the forks can be put into the README of this repo.

## About

Expand Down
1 change: 0 additions & 1 deletion assets/controllers/bootstrap/modal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Controller } from '@hotwired/stimulus';
import { Modal } from 'bootstrap';

/* stimulusFetch: 'lazy' */
export default class extends Controller {
Expand Down
21 changes: 11 additions & 10 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,30 @@
"license": "MIT",
"require": {
"php": "^8.1",
"symfony/framework-bundle": "^6.0|^7.0",
"symfony/translation": "^6.0|^7.0",
"symfony/form": "^6.0|^7.0",
"symfony/mime": "^6.0|^7.0",
"symfony/twig-bundle": "^6.0|^7.0",
"symfony/framework-bundle": "^6.0|^7.0|^8.0",
"symfony/translation": "^6.0|^7.0|^8.0",
"symfony/form": "^6.0|^7.0|^8.0",
"symfony/mime": "^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^6.0|^7.0|^8.0",
"twig/extra-bundle": "^3.6",
"twig/intl-extra": "^3.6",
"symfony/validator": "^6.0|^7.0"
"symfony/validator": "^6.0|^7.0|^8.0"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"friendsofphp/php-cs-fixer": "^3.49",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.11",
"symfony/maker-bundle": "^1.48",
"symfony/security-core": "^6.2|^7.0",
"symfony/security-core": "^6.2|^7.0|^8.0",
"phpoffice/phpspreadsheet": "^1.28",
"doctrine/orm": "^2.15",
"doctrine/doctrine-bundle": "^2.9",
"doctrine/orm": "^2.15|^3.0",
"doctrine/doctrine-bundle": "^2.9|^3.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.4",
"dg/bypass-finals": "dev-master",
"openspout/openspout": "^4.23",
"symfony/http-foundation": "^6.0|^7.0"
"symfony/http-foundation": "^6.0|^7.0|^8.0",
"symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": {
Expand Down
173 changes: 173 additions & 0 deletions docs/src/docs/features/query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Query

A data table needs a query to fetch its rows. You can either pass it from the controller or let the data table type build a default one.

[[toc]]

## Passing the query from the controller

The classic way: the controller provides the query as the second argument of `createDataTable()`.

```php
use App\DataTable\Type\ProductDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
use DataTableFactoryAwareTrait;

public function index(Request $request, ProductRepository $repository): Response
{
$dataTable = $this->createDataTable(
ProductDataTableType::class,
$repository->createQueryBuilder('p'),
);

$dataTable->handleRequest($request);

return $this->render('product/index.html.twig', [
'products' => $dataTable->createView(),
]);
}
}
```

Any value that a registered `ProxyQueryFactoryInterface` supports works (Doctrine `QueryBuilder`, array, etc.).

## Defining a default query in the type

When the controller does not need to customize the query, the type can build it via `createQuery()`:

```php
use App\Repository\ProductRepository;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;

class ProductDataTableType extends AbstractDataTableType
{
public function __construct(
private ProductRepository $repository,
) {
}

public function createQuery(array $options): mixed
{
return $this->repository->createQueryBuilder('p');
}
}
```

The controller can then omit the second argument:

```php
$dataTable = $this->createDataTable(ProductDataTableType::class);
```

`createQuery()` is called **once per data table creation**, so returning a fresh `QueryBuilder` instance each time keeps data tables isolated from one another — mutations from filters, sorting, or pagination on one data table never leak into the next.

The method receives the resolved options, so a type can parameterize the query through its own options:

```php
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefault('active_only', false)
->setAllowedTypes('active_only', 'bool');
}

public function createQuery(array $options): mixed
{
$qb = $this->repository->createQueryBuilder('p');

if ($options['active_only']) {
$qb->andWhere('p.active = true');
}

return $qb;
}
```

```php
$dataTable = $this->createDataTable(ProductDataTableType::class, null, [
'active_only' => true,
]);
```

## Overriding from the controller

An explicit second argument always wins over `createQuery()`. This fully replaces what the type would have returned — the controller owns the query in this case:

```php
$dataTable = $this->createDataTable(ProductDataTableType::class, $customQueryBuilder);
```

There is no mechanism to "grab the type's default query and add conditions on top" — the controller does not have access to the already-built `QueryBuilder` of the type. If you need to extend the default, choose one of the patterns below.

### Option 1 — Parameterize through options

When the controller should influence *how* the type builds its query, expose a type option and branch inside `createQuery()`. This is the canonical way to reuse + extend:

```php
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefault('active_only', false)
->setAllowedTypes('active_only', 'bool');
}

public function createQuery(array $options): mixed
{
$qb = $this->repository->createQueryBuilder('p');

if ($options['active_only']) {
$qb->andWhere('p.active = true');
}

return $qb;
}
```

```php
$dataTable = $this->createDataTable(ProductDataTableType::class, null, [
'active_only' => true,
]);
```

### Option 2 — Share a builder in the repository

When the same base query is needed in several places (type, controller, other services), expose it from the repository and reuse it:

```php
class ProductRepository extends ServiceEntityRepository
{
public function createBaseQueryBuilder(): QueryBuilder
{
return $this->createQueryBuilder('p')
->andWhere('p.deletedAt IS NULL');
}
}
```

```php
// In the type:
public function createQuery(array $options): mixed
{
return $this->repository->createBaseQueryBuilder();
}
```

```php
// In the controller, reusing the same base and adding a condition:
$qb = $repository->createBaseQueryBuilder()
->andWhere('p.stock > 0');

$dataTable = $this->createDataTable(ProductDataTableType::class, $qb);
```

The controller ends up overriding the default (option 1 is not used), but it starts from the same base the type would have returned.

## Parent types

When a type extends another type (via `getParent()`), `createQuery()` uses the child's return value, and falls back to the parent if the child returns `null`. This mirrors how Symfony Forms resolves `empty_data` and similar defaults.
1 change: 1 addition & 0 deletions docs/src/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ If you want to include your application here, open an issue, create a pull reque
- [Exporting](features/exporting.md) with or without applied pagination, filters and personalization
- [Theming](features/theming.md) of every part of the bundle using Twig
- [Data source agnostic](features/extensibility.md) with Doctrine ORM supported out of the box
- [Query](features/query.md) to fetch the data from any source
- [Asynchronicity](features/asynchronicity.md) thanks to integration with Turbo (with prefetching enabled by default)

## Use cases
Expand Down
12 changes: 10 additions & 2 deletions src/DataCollector/DataTableDataCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,19 @@ public function __construct(
}
}

public function __sleep(): array
/**
* @return array<string, mixed>
*/
public function __serialize(): array
{
$this->data = $this->cloneVar($this->data)->withMaxDepth($this->maxDepth);

return parent::__sleep();
if (method_exists(parent::class, '__serialize')) {
return parent::__serialize();
}

// Symfony 7: __serialize() does not exist yet, replicate __sleep() behavior
return ['data' => $this->data];
}

public static function getTemplate(): ?string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Kreyu\Bundle\DataTableBundle\DataTableFactoryInterface;
use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Kreyu\Bundle\DataTableBundle\Type\DataTableTypeInterface;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
Expand Down Expand Up @@ -42,9 +41,9 @@ public function getTypeExtensions(): array
return $this->proxiedType->getTypeExtensions();
}

public function createBuilder(DataTableFactoryInterface $factory, string $name, ?ProxyQueryInterface $query = null, array $options = []): DataTableBuilderInterface
public function createBuilder(DataTableFactoryInterface $factory, string $name, array $options = []): DataTableBuilderInterface
{
$builder = $this->proxiedType->createBuilder($factory, $name, $query, $options);
$builder = $this->proxiedType->createBuilder($factory, $name, $options);
$builder->setAttribute('data_collector/passed_options', $options);
$builder->setType($this);

Expand All @@ -61,6 +60,11 @@ public function createExportView(DataTableInterface $dataTable): DataTableView
return $this->proxiedType->createExportView($dataTable);
}

public function createQuery(array $options): mixed
{
return $this->proxiedType->createQuery($options);
}

public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
$this->proxiedType->buildDataTable($builder, $options);
Expand Down
3 changes: 2 additions & 1 deletion src/DataTableBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ class DataTableBuilder extends DataTableConfigBuilder implements DataTableBuilde
*/
private array $unresolvedExporters = [];

private ?ProxyQueryInterface $query = null;

public function __construct(
string $name,
ResolvedDataTableTypeInterface $type,
private ?ProxyQueryInterface $query = null,
EventDispatcherInterface $dispatcher = new EventDispatcher(),
array $options = [],
) {
Expand Down
10 changes: 7 additions & 3 deletions src/DataTableFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public function createBuilder(string $type = DataTableType::class, mixed $data =

public function createNamedBuilder(string $name, string $type = DataTableType::class, mixed $data = null, array $options = []): DataTableBuilderInterface
{
$type = $this->registry->getType($type);

$builder = $type->createBuilder($this, $name, $options);

$data ??= $type->createQuery($builder->getOptions());

$query = $data;

if (null !== $data && !$data instanceof ProxyQueryInterface) {
Expand All @@ -42,9 +48,7 @@ public function createNamedBuilder(string $name, string $type = DataTableType::c
}
}

$type = $this->registry->getType($type);

$builder = $type->createBuilder($this, $name, $query, $options);
$builder->setQuery($query);

$type->buildDataTable($builder, $builder->getOptions());

Expand Down
19 changes: 16 additions & 3 deletions src/Request/HttpFoundationRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private function filter(DataTableInterface $dataTable, Request $request): void

$form = $dataTable->createFiltrationFormBuilder()->getForm();

if ($data = $request->get($form->getName())) {
if ($data = $this->getRequestParameter($request, $form->getName())) {
$form->submit($data);
}

Expand Down Expand Up @@ -105,7 +105,7 @@ private function personalize(DataTableInterface $dataTable, Request $request): v

$form = $dataTable->createPersonalizationFormBuilder()->getForm();

if ($data = $request->get($form->getName())) {
if ($data = $this->getRequestParameter($request, $form->getName())) {
$form->submit($data);
}

Expand All @@ -122,7 +122,7 @@ private function export(DataTableInterface $dataTable, Request $request): void

$form = $dataTable->createExportFormBuilder()->getForm();

if ($data = $request->get($form->getName())) {
if ($data = $this->getRequestParameter($request, $form->getName())) {
$form->submit($data);
}

Expand All @@ -131,6 +131,19 @@ private function export(DataTableInterface $dataTable, Request $request): void
}
}

private function getRequestParameter(Request $request, string $name): mixed
{
if ($request->query->has($name)) {
return $request->query->all()[$name];
}

if ($request->request->has($name)) {
return $request->request->all()[$name];
}

return null;
}

private function extractQueryParameter(Request $request, string $path): mixed
{
return $this->propertyAccessor->getValue($request->query->all(), $path);
Expand Down
3 changes: 2 additions & 1 deletion src/Twig/DataTableExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Kreyu\Bundle\DataTableBundle\ValueRowView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Error\Error as TwigException;
use Twig\Error\RuntimeError;
Expand Down Expand Up @@ -71,7 +72,7 @@ public function getFunctions(): array
$functions[] = new TwigFunction('data_table_form_aware', $this->renderDataTableFormAware(...), [
'needs_environment' => true,
'is_safe' => ['html'],
'deprecated' => true,
'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.15'),
]);

return $functions;
Expand Down
Loading
Loading