Skip to content

Commit

Permalink
Merge branch 'release/v1.0.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
tuxes3 committed Aug 15, 2023
2 parents 5ee389b + 5f8145a commit 8079a54
Show file tree
Hide file tree
Showing 42 changed files with 900 additions and 193 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# CHANGELOG

## v1.0.1
- English translation
- Improved & Documented `ArrayDataLoader`
- Filters are loaded over Dependency Injection
- Batch Actions are defined on the table
- Lots of styling improvements
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ install:

## fix php code style
ecs:
vendor/bin/ecs --fix
vendor/bin/ecs --fix --clear-cache

## check code with phpstan
phpstan:
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"symfony/validator": "^5.4|^6.0",
"araise/core-bundle": "^1.0",
"araise/search-bundle": "^3.0",
"phpoffice/phpspreadsheet": "^1.22"
"phpoffice/phpspreadsheet": "^1.22",
"symfony/stimulus-bundle": "^2.10",
"coduo/php-to-string": "^3.2"
},
"require-dev": {
"symfony/phpunit-bridge": "^v6.0",
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
* [Filters](filters.md)
* [Formatter](formatter.md)
* [Events](events.md)
* [Table from array](array-data-laoder.md)
81 changes: 81 additions & 0 deletions docs/array-data-laoder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Array Data Loader

The standard data loader is the `DoctrineDataLoader`. It loads data from a database table.
Also when using the full araise framework, mostly the data is stored in a database.

There are cases where you just need to dispaly a table with data from an array.
For this case the `ArrayDataLoader` is the right choice.

## Usage
`Controller/DefaultController.php`:
```php
namespace App\Controller;

use araise\TableBundle\DataLoader\ArrayDataLoader;
use araise\TableBundle\Factory\TableFactory;
use araise\TableBundle\Table\Table;
use Doctrine\Common\Collections\ArrayCollection;
use Faker\Factory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{

public function __construct(protected TableFactory $tableFactory)
{
}

#[Route('/', name: 'index')]
public function indexAction(): Response
{
$table = $this->tableFactory->create('project', ArrayDataLoader::class, [
Table::OPT_DATALOADER_OPTIONS => [
ArrayDataLoader::OPT_DATA => $this->getTestDataArray(),
],
]);
$table
->addColumn('Name', null, [
'accessor_path' => '[name]',
])
->addColumn('Phone', null, [
'accessor_path' => '[phone]',
])
->addColumn('Firma', null, [
'accessor_path' => '[company]',
])
;

return $this->render('index.html.twig', [
'table' => $table,
]);
}

protected function getTestDataArray(int $many = 200): ArrayCollection
{
$faker = Factory::create();
$faker->seed(1234); // to always get the same data
$col = new ArrayCollection();
for ($i = 1; $i <= $many; $i++) {
$col->add([
'name' => $faker->name(),
'phone' => $faker->phoneNumber(),
'company' => $faker->company(),
]);
}
return $col;
}

}
```
`templates/index.html.twig`:
```twig
{% extends 'base.html.twig' %}
{% block body %}
{{ araise_table_render(table) }}
{% endblock %}
```

You could also use `araise_table_only_render` in your twig template, this will skip the pagination and filters.
23 changes: 16 additions & 7 deletions docs/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ You may also consult the chapter [FilterType Examples](#filtertype-examples) for

```php
$table->getFilterExtension()
->addFilter('firstname', 'Firstname', new TextFilterType('column'));
->addFilterType('firstname', 'Firstname', TextFilterType::class);
// addFilter(acronym, label, FilterType)
```

Expand All @@ -26,26 +26,35 @@ Here are some more examples on how to create your own custom filters by using th
The number filter type allows you to filter your data by a column which holds a number.

```php
$table->getFilterExtension()->addFilter('age', 'Age', new NumberFilterType(self::getQueryAlias() . '.age'))
$table->getFilterExtension()->addFilterType('age', 'Age', NumberFilterType::class, [
FilterType::OPT_COLUMN => (self::getQueryAlias() . '.age')
])
```

### ChoiceFilterType

With this filter type you can create a dropdown with predefined values for filtering your data.

```php
$table->getFilterExtension()->addFilter('state', 'State', new ChoiceFilterType('s.state', ['open', 'in_progress', 'done'], ['s' => self::getQueryAlias() . '.task']))
$table->getFilterExtension()->addFilterType('state', 'State', ChoiceFilterType::class, [
FilterType::OPT_COLUMN => 's.state',
ChoiceFilterType::OPT_CHOICES => ['open', 'in_progress', 'done'],
FilterType::OPT_JOINS => ['s' => self::getQueryAlias() . '.task'])
)
```

### AjaxRelationFilterType

This filter allows you to request the data for the dropdown via AJAX. It will create a filter from a relation between two entities.

```php
$table->getFilterExtension()->addFilter('city', 'City', new AjaxRelationFilterType('c', City::class, $entityManager, $router->generate(CityDefinition::getRoute(Page::JSONSEARCH)), [
's' => $queryAlias . '.street',
'c' => 's.city',
]));
$table->getFilterExtension()->addFilterType('city', 'City', AjaxRelationFilterType::class, [
FilterType::OPT_COLUMN => 'c',
AjaxRelationFilterType::OPT_TARGET_ENTITY => City::class,
AjaxRelationFilterType::OPT_JSON_SEARCH_URL => $router->generate(CityDefinition::getRoute(Page::JSONSEARCH)), [
's' => $queryAlias . '.street',
'c' => 's.city',
]]);
```

Let's say for this example we have a `BuildingDefinition` where we want to filter the buildings by their city. The city is stored as a property of the `Street` entity for the purpose of this example.
Expand Down
10 changes: 10 additions & 0 deletions src/DataLoader/AbstractDataLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ public function configureOptions(OptionsResolver $resolver): void
{
}

public function getNext(mixed $current): mixed
{
return null;
}

public function getPrev(mixed $current): mixed
{
return null;
}

public function getOption(string $option)
{
return $this->options[$option];
Expand Down
49 changes: 48 additions & 1 deletion src/DataLoader/ArrayDataLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,62 @@

namespace araise\TableBundle\DataLoader;

use araise\TableBundle\Extension\PaginationExtension;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArrayDataLoader extends AbstractDataLoader
{
public const OPT_DATA = 'data';

protected PaginationExtension $paginationExtension;

public function getNext(mixed $current): mixed
{
$data = $this->options[self::OPT_DATA]->toArray();
$next = false;
foreach ($data as $item) {
if ($next) {
return $item;
}
$next = $item === $current;
}
return null;
}

public function getPrev(mixed $current): mixed
{
$data = $this->options[self::OPT_DATA]->toArray();
$prev = null;
foreach ($data as $item) {
if ($item === $current) {
return $prev;
}
$prev = $item;
}
return null;
}

public function loadNecessaryExtensions(iterable $extensions): void
{
foreach ($extensions as $extension) {
if ($extension instanceof PaginationExtension) {
$this->paginationExtension = $extension;
break;
}
}
}

public function getResults(): iterable
{
return $this->options[self::OPT_DATA];
$this->paginationExtension->setTotalResults(count($this->options[self::OPT_DATA]));
$data = $this->options[self::OPT_DATA]->toArray();
$data = array_splice(
$data,
$this->paginationExtension->getOffsetResults(),
$this->paginationExtension->getLimit()
);
return new ArrayCollection($data);
}

public function configureOptions(OptionsResolver $resolver): void
Expand Down
4 changes: 4 additions & 0 deletions src/DataLoader/DataLoaderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ public function getResults(): iterable;
public function getOption(string $option);

public function loadNecessaryExtensions(iterable $extensions): void;

public function getNext(mixed $current): mixed;

public function getPrev(mixed $current): mixed;
}
100 changes: 99 additions & 1 deletion src/DataLoader/DoctrineDataLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@
namespace araise\TableBundle\DataLoader;

use araise\TableBundle\Extension\PaginationExtension;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DoctrineDataLoader extends AbstractDataLoader
{
public const OPT_QUERY_BUILDER = 'query_builder';

public const OPT_SAVE_LAST_QUERY = 'save_last_query';

protected const LAST_TABLE_QUERY = 'last_table_query';

protected PaginationExtension $paginationExtension;

public function __construct(
protected EntityManagerInterface $entityManager,
protected ?RequestStack $requestStack
) {
}

public function loadNecessaryExtensions(iterable $extensions): void
{
foreach ($extensions as $extension) {
Expand All @@ -29,9 +42,21 @@ public function getResults(): iterable
{
/** @var QueryBuilder $qb */
$qb = (clone $this->options[self::OPT_QUERY_BUILDER]);
$qb->select('COUNT(' . $qb->getRootAliases()[0] . ')');
$qb->select('COUNT('.$qb->getRootAliases()[0].')');
$this->paginationExtension->setTotalResults((int) $qb->getQuery()->getSingleScalarResult());

if ($this->getOption(self::OPT_SAVE_LAST_QUERY) && $this->requestStack->getCurrentRequest()?->hasSession()) {
/** @var QueryBuilder $qbSave */
$qbSave = (clone $this->options[self::OPT_QUERY_BUILDER]);
$sql = $qbSave->select($qbSave->getRootAliases()[0].'.id')->getQuery()->getSql();
$param = $qbSave->getParameters();
$this->requestStack->getSession()->set(self::LAST_TABLE_QUERY, serialize([
'sql' => $sql,
'param' => $param,
'entity' => $qbSave->getDQLPart('from')[0]->getFrom(),
]));
}

if ($this->paginationExtension->getLimit()) {
$this->options[self::OPT_QUERY_BUILDER]
->setMaxResults($this->paginationExtension->getLimit())
Expand All @@ -45,10 +70,83 @@ public function getResults(): iterable
return $paginator->getIterator();
}

public function getNext(mixed $current): mixed
{
$sessionNext = $this->getSessionNextPrevData($current);

if ($sessionNext === null) {
return null;
}
$id = $this->getIdOf($current);
$next = false;
foreach ($sessionNext as $item) {
if ($next) {
return $this->entityManager->getRepository($current::class)->find($item['id']);
}
if ($item['id'] === $id) {
$next = true;
}
}
return null;
}

public function getPrev(mixed $current): mixed
{
$sessionNext = $this->getSessionNextPrevData($current);

if ($sessionNext === null) {
return null;
}
$id = $this->getIdOf($current);
$last = null;
foreach ($sessionNext as $item) {
if ($item['id'] === $id) {
return $last === null ? null : $this->entityManager->getRepository($current::class)->find($last['id']);
}
$last = $item;
}
return null;
}

public function getIdOf(mixed $current): mixed
{
try {
return $current->getId();
} catch (\Exception|\Error $e) {
return null;
}
}

public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setRequired(self::OPT_QUERY_BUILDER);
$resolver->setRequired(self::OPT_SAVE_LAST_QUERY);
$resolver->setDefault(self::OPT_SAVE_LAST_QUERY, false);
$resolver->setAllowedTypes(self::OPT_QUERY_BUILDER, QueryBuilder::class);
}

protected function getSessionNextPrevData(mixed $current): ?array
{
if (!$this->requestStack->getCurrentRequest()?->hasSession()) {
return null;
}
$lastQuery = $this->requestStack?->getSession()->get(self::LAST_TABLE_QUERY);
if ($lastQuery === null) {
return null;
}
$lastQuery = unserialize($lastQuery);

if ($current::class !== $lastQuery['entity']) {
return null;
}

$rsm = new ResultSetMapping();
$rsm->addScalarResult('id_0', 'id');
$query = $this->entityManager->createNativeQuery($lastQuery['sql'], $rsm);
foreach ($lastQuery['param'] as $i => $value) {
$query->setParameter($i + 1, $value->getValue(), $value->getType());
}
return $query->getResult();
}
}
Loading

0 comments on commit 8079a54

Please sign in to comment.