Skip to content

Commit

Permalink
Improve ActionsColumnType & CollectionColumnType, overhaul profiler i…
Browse files Browse the repository at this point in the history
…ntegration (#130)

Improve ActionsColumnType & CollectionColumnType, overhaul profiler integration
  • Loading branch information
Kreyu authored Sep 21, 2024
1 parent ac7809e commit a5de2c3
Show file tree
Hide file tree
Showing 39 changed files with 3,249 additions and 751 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default defineConfig({
{ text: 'Persistence', link: '/docs/features/persistence' },
{ text: 'Theming', link: '/docs/features/theming' },
{ text: 'Asynchronicity', link: '/docs/features/asynchronicity' },
{ text: 'Profiler', link: '/docs/features/profiler' },
{ text: 'Extensibility', link: '/docs/features/extensibility' },
]
},
Expand Down
1,605 changes: 1,063 additions & 542 deletions docs/package-lock.json

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions docs/src/docs/features/profiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Profiler

The bundle has a built-in integration with the [Symfony Profiler](https://symfony.com/doc/current/profiler.html).

## Usage

If at least one data table was created, the toolbar will include a new tab:

![profiler_toolbar.png](/profiler_toolbar.png)

Clicking it will redirect you to the _Data Tables_ profiler tab:

![profiler_tab.png](/profiler_tab.png)

Here you can inspect every single part of each data table:

- quick overview - is this column sortable? is this filter applied?
- type class of each component;
- which options were passed;
- how those options got resolved;
- variables available in views, passed to the templates;
- data of each value row of the current page;

## Configuration

Because the amount of data collected for this integration can be massive,
the maximum depth of serialization can be adjusted in the bundle configuration:

```yaml
kreyu_data_table:
profiler:
max_depth: 3
```
Increasing the `max_depth` value will result in collecting and displaying deeper objects.
If you wish to disable this limitation completely, set this value to `-1`.

::: warning
Increasing the depth **will** result in the browser freezes after opening the profiler tab.
:::
Binary file added docs/src/public/profiler_tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/public/profiler_toolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
parameters:
level: 5
checkGenericClassInNonGenericObjectType: false
paths:
- src
- tests
excludePaths:
- src/Resources/skeleton/*
- src/DependencyInjection/Configuration.php
- src/DependencyInjection/Configuration.php
5 changes: 4 additions & 1 deletion src/Column/Type/ActionsColumnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a
$actions = [];

foreach ($options['actions'] as $name => $action) {
$actions[$name] = $this->resolveAction($name, $action, $view)?->createView($view);
$action = $this->resolveAction($name, $action, $view);
$action?->setDataTable($column->getDataTable());

$actions[$name] = $action?->createView($view);
}

$view->vars['actions'] = array_filter($actions);
Expand Down
5 changes: 3 additions & 2 deletions src/Column/Type/CollectionColumnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ private function createChildrenColumnValueViews(ColumnValueView $view, ColumnInt
/** @var ColumnFactoryInterface $prototypeFactory */
$prototypeFactory = $column->getConfig()->getAttribute('prototype_factory');

$prototype = $prototypeFactory->createNamed('__name__', $options['entry_type'], $options['entry_options']);

$children = [];

foreach ($view->vars['value'] ?? [] as $index => $data) {
Expand All @@ -73,6 +71,9 @@ private function createChildrenColumnValueViews(ColumnValueView $view, ColumnInt
$valueRowView->index = $index;
$valueRowView->data = $data;

$prototype = $prototypeFactory->createNamed((string) $index, $options['entry_type'], $options['entry_options']);
$prototype->setDataTable($column->getDataTable());

$children[] = $prototype->createValueView($valueRowView);
}

Expand Down
181 changes: 126 additions & 55 deletions src/DataCollector/DataTableDataCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@

namespace Kreyu\Bundle\DataTableBundle\DataCollector;

use Kreyu\Bundle\DataTableBundle\Action\ActionContext;
use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
use Kreyu\Bundle\DataTableBundle\Action\ActionView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -17,104 +26,166 @@
class DataTableDataCollector extends AbstractDataCollector implements DataTableDataCollectorInterface
{
public function __construct(
readonly private DataTableDataExtractorInterface $dataExtractor,
private DataTableDataExtractorInterface $dataExtractor,
private int $maxDepth = 3,
) {
if (!class_exists(ClassStub::class)) {
throw new \LogicException(sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__));
}
}

public function __sleep(): array
{
$this->data = $this->cloneVar($this->data)->withMaxDepth($this->maxDepth);

return parent::__sleep();
}

public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// Everything is collected on dataTable creation
}

public function collectDataTable(DataTableInterface $dataTable): void
{
$this->data[$dataTable->getConfig()->getName()] = [
'filters' => [],
'columns' => array_map(function (ColumnInterface $column) {
return [
'name' => $column->getName(),
'type' => $column->getConfig()->getType()->getInnerType()::class,
'options' => $this->cloneVar($column->getConfig()->getOptions()),
];
}, array_filter($dataTable->getColumns(), function (ColumnInterface $column) {
return !str_contains($column->getName(), '__');
}),
$data = [
'columns' => $this->mapWithKeys(
fn (ColumnInterface $column) => [$column->getName() => $this->dataExtractor->extractColumnConfiguration($column)],
$dataTable->getColumns(),
),
'filters' => $this->mapWithKeys(
fn (FilterInterface $filter) => [$filter->getName() => $this->dataExtractor->extractFilterConfiguration($filter)],
$dataTable->getFilters(),
),
'actions' => $this->mapWithKeys(
fn (ActionInterface $action) => [$action->getName() => $this->dataExtractor->extractActionConfiguration($action)],
$dataTable->getActions(),
),
'row_actions' => $this->mapWithKeys(
fn (ActionInterface $action) => [$action->getName() => $this->dataExtractor->extractActionConfiguration($action)],
$dataTable->getRowActions(),
),
'batch_actions' => $this->mapWithKeys(
fn (ActionInterface $action) => [$action->getName() => $this->dataExtractor->extractActionConfiguration($action)],
$dataTable->getBatchActions(),
),
'exporters' => $this->mapWithKeys(
fn (ExporterInterface $exporter) => [$exporter->getName() => $this->dataExtractor->extractExporterConfiguration($exporter)],
$dataTable->getExporters(),
),
'actions' => array_map(function (ActionInterface $action) {
return [
'name' => $action->getName(),
'type' => $action->getConfig()->getType()->getInnerType()::class,
'options' => $this->cloneVar($action->getConfig()->getOptions()),
];
}, $dataTable->getActions()),
'batch_actions' => array_map(function (ActionInterface $action) {
return [
'name' => $action->getName(),
'type' => $action->getConfig()->getType()->getInnerType()::class,
'options' => $this->cloneVar($action->getConfig()->getOptions()),
];
}, $dataTable->getBatchActions()),
'row_actions' => array_map(function (ActionInterface $action) {
return [
'name' => $action->getName(),
'type' => $action->getConfig()->getType()->getInnerType()::class,
'options' => $this->cloneVar($action->getConfig()->getOptions()),
];
}, $dataTable->getRowActions()),
];

$data = array_merge($data, $this->dataExtractor->extractDataTableConfiguration($dataTable));

$this->data[$dataTable->getName()] = $data;
}

public static function getTemplate(): ?string
public function collectDataTableView(DataTableInterface $dataTable, DataTableView $view): void
{
return '@KreyuDataTable/data_collector/template.html.twig';
$this->data[$dataTable->getName()] += [
'view_vars' => $this->ksort($view->vars),
'value_rows' => $this->dataExtractor->extractValueRows($view),
];
}

public function getDataTables(): array
public function collectColumnHeaderView(ColumnInterface $column, ColumnHeaderView $view): void
{
return array_keys($this->data);
$this->data[$column->getDataTable()->getName()]['columns'][$column->getName()]['header_view_vars'] = $this->ksort($view->vars);
}

public function getColumns(string $dataTableName): array
public function collectColumnValueView(ColumnInterface $column, ColumnValueView $view): void
{
return $this->data[$dataTableName]['columns'];
// TODO: Support nested columns from CollectionColumnType
if (null !== $view->parent->origin) {
return;
}

$this->data[$column->getDataTable()->getName()]['columns'][$column->getName()]['value_view_vars'] = $this->ksort($view->vars);
}

public function getFilters(string $dataTableName): array
public function collectSortingData(DataTableInterface $dataTable, SortingData $data): void
{
return $this->data[$dataTableName]['filters'];
foreach ($data->getColumns() as $columnName => $columnSortingData) {
if (!$dataTable->hasColumn($columnName)) {
continue;
}

$column = $dataTable->getColumn($columnName);

$this->data[$column->getDataTable()->getName()]['columns'][$column->getName()] ??= [];
$this->data[$column->getDataTable()->getName()]['columns'][$column->getName()] += [
'sort_direction' => $columnSortingData->getDirection(),
];
}
}

public function getActions(string $dataTableName): array
public function collectFilterView(FilterInterface $filter, FilterView $view): void
{
return $this->data[$dataTableName]['actions'];
$this->data[$filter->getDataTable()->getName()]['filters'][$filter->getName()]['view_vars'] = $this->ksort($view->vars);
}

public function getBatchActions(string $dataTableName): array
public function collectFiltrationData(DataTableInterface $dataTable, FiltrationData $data): void
{
return $this->data[$dataTableName]['batch_actions'];
foreach ($data->getFilters() as $filterName => $filterData) {
if (!$dataTable->hasFilter($filterName)) {
continue;
}

$filter = $dataTable->getFilter($filterName);

$this->data[$filter->getDataTable()->getName()]['filters'][$filter->getName()] ??= [];
$this->data[$filter->getDataTable()->getName()]['filters'][$filter->getName()] += [
'data' => $filterData,
'operator_label' => $filterData->getOperator()->getLabel(),
];
}
}

public function getRowActions(string $dataTableName): array
public function collectActionView(ActionInterface $action, ActionView $view): void
{
return $this->data[$dataTableName]['row_actions'];
$actionsKey = match ($action->getConfig()->getContext()) {
ActionContext::Global => 'actions',
ActionContext::Row => 'row_actions',
ActionContext::Batch => 'batch_actions',
};

$this->data[$action->getDataTable()->getName()][$actionsKey][$action->getName()]['view_vars'] = $this->ksort($view->vars);
}

public function collectFilter(DataTableInterface $dataTable, FiltrationData $filtrationData): void
public static function getTemplate(): ?string
{
$dataToRedirect = [];
return '@KreyuDataTable/data_collector/template.html.twig';
}

foreach ($filtrationData->getFilters() as $field => $data) {
$dataToRedirect[] = $this->dataExtractor->extractFilter($dataTable, $field, $data);
public function getData(): array|Data
{
return $this->data;
}

/**
* @internal
*/
private function mapWithKeys(callable $callback, array $array): array
{
$data = [];

foreach ($array as $value) {
foreach ($callback($value) as $mapKey => $mapValue) {
$data[$mapKey] = $mapValue;
}
}

$this->data[$dataTable->getConfig()->getName()]['filters'] = $dataToRedirect;
return $data;
}

public function getData(): array|Data
/**
* @internal
*/
private function ksort(array $array): array
{
return $this->data;
$copy = $array;

ksort($copy);

return $copy;
}
}
25 changes: 24 additions & 1 deletion src/DataCollector/DataTableDataCollectorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,37 @@

namespace Kreyu\Bundle\DataTableBundle\DataCollector;

use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
use Kreyu\Bundle\DataTableBundle\Action\ActionView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\VarDumper\Cloner\Data;

interface DataTableDataCollectorInterface extends DataCollectorInterface
{
public function collectFilter(DataTableInterface $dataTable, FiltrationData $filtrationData): void;
public function collectDataTable(DataTableInterface $dataTable): void;

public function collectDataTableView(DataTableInterface $dataTable, DataTableView $view): void;

public function collectColumnHeaderView(ColumnInterface $column, ColumnHeaderView $view): void;

public function collectColumnValueView(ColumnInterface $column, ColumnValueView $view): void;

public function collectSortingData(DataTableInterface $dataTable, SortingData $data): void;

public function collectFilterView(FilterInterface $filter, FilterView $view): void;

public function collectFiltrationData(DataTableInterface $dataTable, FiltrationData $data): void;

public function collectActionView(ActionInterface $action, ActionView $view): void;

public function getData(): array|Data;
}
Loading

0 comments on commit a5de2c3

Please sign in to comment.