Skip to content

Commit 2746dda

Browse files
Add Count column (#17)
Add CountColumn Add CountFilter Change "search" callback function signature
1 parent 2cd3695 commit 2746dda

10 files changed

Lines changed: 240 additions & 35 deletions

README.md

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ Inspired by [SgDatatablesBundle](https://github.com/stwe/DatatablesBundle).
1616
2. [BooleanColumn](#booleancolumn)
1717
3. [DateTimeColumn](#datetimecolumn)
1818
4. [HiddenColumn](#hiddencolumn)
19-
5. [ActionColumn](#actioncolumn)
19+
5. [CountColumn](#countcolumn)
20+
6. [ActionColumn](#actioncolumn)
2021
5. [Filters](#filters)
2122
1. [TextFilter](#textfilter)
2223
2. [ChoiceFilter](#choicefilter)
2324
3. [BooleanChoiceFilter](#booleanchoicefilter)
25+
4. [CountFilter](#countfilter)
2426
6. [Configuration](#configuration)
2527
1. [Table Dataset Options](#table-dataset-options)
2628
2. [Table Options](#table-options)
@@ -269,7 +271,6 @@ Represents column with text. With formatter you can create complex columns.
269271
| visible | bool | true | show / hide column |
270272
| emptyData | string | "" | default value if attribute from entity is null |
271273
| sort | Closure / null | null | custom sort query callback (see example) |
272-
| filter | Closure / null | null | custom filter query callback (see example) |
273274
| data | Closure / null | null | custom data callback (see example) |
274275
| addIf | Closure | ` function() {return true;}` | In this callback it is decided if the column will be rendered. |
275276
| align | string / null | null | Indicate how to align the column data. `'left'`, `'right'`, `'center'` can be used. |
@@ -300,28 +301,24 @@ use Doctrine\ORM\QueryBuilder;
300301
$qb->addOrderBy('username', $direction);
301302
},
302303

303-
'search' => function (Composite $composite, QueryBuilder $qb, $dql, $search, $key) {
304+
'search' => function (Composite $composite, QueryBuilder $qb, $search) {
304305
//first add condition to $composite
305-
//don't forget the '?' before $key
306-
$composite->add($qb->expr()->like($dql, '?' . $key));
306+
//don't forget the ':' before the parameter for binding
307+
$composite->add($qb->expr()->like($dql, ':username'));
307308

308309
//then bind search to query
309-
$qb->setParameter($key, '%' . $search . '%');
310+
$qb->setParameter("username", $search . '%');
310311
}
311312
))
312313
```
313314

314315
**search** Option:
315316

316-
The search option seems a bit complicated at first, but it allows full control over the query in the column.
317-
318317
| Paramenter name | Description |
319318
| ---------------------- | ------------------------------------------------------------ |
320-
| `Composite $composite` | In the global search all columns are connected as or. In the advanced search all columns are combined with an and-connection. With `$composite` more parts can be added to the query. |
319+
| `Composite $composite` | In the global search all columns are connected as or. In the advanced search all columns are combined with an and-connection. With `$composite` more parts can be added to the query. `Composite` is the parent class of `AndX` and `OrX`. |
321320
| `QueryBuilder $qb` | `$qb` holds the use QueryBuilder. It is the same instance as can be queried with `getQueryBuilder()` in the table class. |
322-
| `$dql` | `$dql` represents the "path" to the variable in the query (e.g. `user.username` or in case of a JOIN `costCentre.name`) |
323321
| `$search` | The search in the type of a string. |
324-
| `$key` | The index of the columns already gone through. The index is used for parameter binding to the query. |
325322

326323

327324

@@ -347,6 +344,8 @@ All options of TextColumn.
347344
#### Example
348345

349346
```php
347+
use HelloSebastian\HelloBootstrapTableBundle\Columns\BooleanColumn;
348+
350349
->add('isActive', BooleanColumn::class, array(
351350
'title' => 'is active',
352351
'trueLabel' => 'yes',
@@ -373,6 +372,8 @@ All Options of TextColumn
373372
#### Example
374373

375374
```php
375+
use HelloSebastian\HelloBootstrapTableBundle\Columns\DateTimeColumn;
376+
376377
->add('createdAt', DateTimeColumn::class, array(
377378
'title' => 'Created at',
378379
'format' => 'd.m.Y'
@@ -394,11 +395,47 @@ All Options of TextColumn.
394395
#### Example
395396

396397
```php
398+
use HelloSebastian\HelloBootstrapTableBundle\Columns\HiddenColumn;
399+
397400
->add("id", HiddenColumn::class)
398401
```
399402

400403

401404

405+
### CountColumn
406+
407+
Represents column for counting OneToMany relations (for ArrayCollection attributes).
408+
409+
*Only works with direct attributes so far. For example, "users" would work in a `DepartmentTable`, but "users.items" would not.*
410+
411+
#### Options
412+
413+
All Options of TextColumn.
414+
415+
`filter` is set to `array(CountFilter::class, array())` by default.
416+
417+
#### Example
418+
419+
```php
420+
// App\Entity\Department.php
421+
422+
/**
423+
* @var ArrayCollection|User[]
424+
* @ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="department")
425+
*/
426+
private $users;
427+
428+
429+
// App\HelloTable\UserTable.php
430+
use HelloSebastian\HelloBootstrapTableBundle\Columns\CountColumn;
431+
432+
->add('users', CountColumn::class, array(
433+
'title' => 'Users'
434+
))
435+
```
436+
437+
438+
402439
### ActionColumn
403440

404441
Represents column for action buttons (show / edit / remove ...).
@@ -509,6 +546,8 @@ use HelloSebastian\HelloBootstrapTableBundle\Filters\TextFilter;
509546
))
510547
```
511548

549+
550+
512551
### ChoiceFilter
513552

514553
With the ChoiceFilter you can create a `select` input field.
@@ -542,6 +581,8 @@ use HelloSebastian\HelloBootstrapTableBundle\Filters\ChoiceFilter;
542581
))
543582
```
544583

584+
585+
545586
### BooleanChoiceFilter
546587

547588
BooleanChoiceFilter is a special `ChoiceFilter` with default choices and query expression. The expression is optimized for boolean values.
@@ -581,6 +622,37 @@ If not `choices` is set to:
581622

582623

583624

625+
### CountFilter
626+
627+
With the CountFilter you can filtering and sorting by counting OneToMany relations.
628+
629+
#### Options
630+
631+
All Options from TextFilter.
632+
633+
**And**:
634+
635+
| Option | Type | Default | Description |
636+
| ---------- | ------ | ------- | ------------------------------------------------------------ |
637+
| condition | string | "gte" | Operation to compare counting.<br /> Available options: "gt", "gte", "eq", "neq", "lt", "lte" |
638+
| primaryKey | string | "id" | Primary key of the target entity in the OneToMany relation. <br />For example: A user is in one deparment. One department has many users. In the user entity there is a `$department` attribute that is pointing to the department entity. With this option you specify the primary key of the department entity (the target entity). |
639+
640+
#### Example
641+
642+
```php
643+
use HelloSebastian\HelloBootstrapTableBundle\Filters\CountFilter;
644+
645+
->add('users', CountColumn::class, array(
646+
'title' => 'Users',
647+
'filter' => array(CountFilter::class, array(
648+
'condition' => 'lte',
649+
'primaryKey' => 'uuid'
650+
))
651+
))
652+
```
653+
654+
655+
584656
## Configuration
585657

586658

src/Columns/AbstractColumn.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ protected function configureInternalOptions(OptionsResolver $resolver)
235235
$resolver->setAllowedTypes('data', ['Closure', 'null']);
236236
$resolver->setAllowedTypes('sort', ['Closure', 'null']);
237237
$resolver->setAllowedTypes('search', ['Closure', 'null']);
238-
$resolver->setAllowedTypes('filter', ['array', 'null']);
238+
$resolver->setAllowedTypes('filter', ['array']);
239239
$resolver->setAllowedTypes('addIf', ['Closure']);
240240
}
241241

src/Columns/ActionColumn.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ public function buildData($entity)
3232
$item = array();
3333

3434
/**
35-
* @var int $key
3635
* @var ActionButton $button
3736
*/
38-
foreach ($this->outputOptions['buttons'] as $key => $button) {
37+
foreach ($this->outputOptions['buttons'] as $button) {
3938
if ($button->getAddIfCallback()()) {
4039
$routeParams = array();
4140
foreach ($button->getRouteParams() as $param) {

src/Columns/CountColumn.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace HelloSebastian\HelloBootstrapTableBundle\Columns;
4+
5+
use Doctrine\Common\Collections\Collection;
6+
use Doctrine\ORM\QueryBuilder;
7+
use HelloSebastian\HelloBootstrapTableBundle\Filters\CountFilter;
8+
use Symfony\Component\OptionsResolver\OptionsResolver;
9+
10+
class CountColumn extends AbstractColumn
11+
{
12+
13+
protected function configureInternalOptions(OptionsResolver $resolver)
14+
{
15+
parent::configureInternalOptions($resolver);
16+
17+
$resolver->setDefaults(array(
18+
"filter" => array(CountFilter::class, array())
19+
));
20+
}
21+
22+
public function buildData($entity)
23+
{
24+
if (!$this->propertyAccessor->isReadable($entity, $this->getDql())) {
25+
return $this->getEmptyData();
26+
}
27+
28+
$collection = $this->propertyAccessor->getValue($entity, $this->getDql());
29+
30+
if (is_null($collection)) {
31+
return $this->getEmptyData();
32+
}
33+
34+
if (!$collection instanceof Collection) {
35+
throw new \LogicException("Value should be implemented the interface Doctrine\Common\Collections\Collection. Type: " . gettype($collection));
36+
}
37+
38+
return $collection->count();
39+
}
40+
}

src/Filters/AbstractFilter.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace HelloSebastian\HelloBootstrapTableBundle\Filters;
55

66

7+
use Doctrine\ORM\Mapping\ClassMetadata;
78
use Doctrine\ORM\Query\Expr\Composite;
89
use Doctrine\ORM\QueryBuilder;
910
use HelloSebastian\HelloBootstrapTableBundle\Columns\AbstractColumn;
@@ -46,7 +47,12 @@ protected function configureOptions(OptionsResolver $resolver)
4647
$resolver->setAllowedTypes("placeholder", ["string", "null"]);
4748
}
4849

49-
public abstract function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key);
50+
public abstract function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key, ClassMetadata $metadata);
51+
52+
public function addOrder(QueryBuilder $qb, $dql, $direction, ClassMetadata $metadata)
53+
{
54+
$qb->addOrderBy($dql, $direction);
55+
}
5056

5157
public function getOptions()
5258
{

src/Filters/BooleanChoiceFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace HelloSebastian\HelloBootstrapTableBundle\Filters;
55

66

7+
use Doctrine\ORM\Mapping\ClassMetadata;
78
use Doctrine\ORM\Query\Expr\Composite;
89
use Doctrine\ORM\QueryBuilder;
910
use HelloSebastian\HelloBootstrapTableBundle\Columns\AbstractColumn;
@@ -41,7 +42,7 @@ protected function configureOptions(OptionsResolver $resolver)
4142
}
4243

4344

44-
public function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key)
45+
public function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key, ClassMetadata $metadata)
4546
{
4647
if ($search == "null") {
4748
return;

src/Filters/ChoiceFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace HelloSebastian\HelloBootstrapTableBundle\Filters;
55

66

7+
use Doctrine\ORM\Mapping\ClassMetadata;
78
use Doctrine\ORM\Query\Expr\Composite;
89
use Doctrine\ORM\QueryBuilder;
910
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -22,7 +23,7 @@ protected function configureOptions(OptionsResolver $resolver)
2223
$resolver->setAllowedTypes("choices", ["array"]);
2324
}
2425

25-
public function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key)
26+
public function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key, ClassMetadata $metadata)
2627
{
2728
if ($search == "null") {
2829
return;

src/Filters/CountFilter.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace HelloSebastian\HelloBootstrapTableBundle\Filters;
4+
5+
use Doctrine\ORM\Mapping\ClassMetadata;
6+
use Doctrine\ORM\Query\Expr\Composite;
7+
use Doctrine\ORM\QueryBuilder;
8+
use Symfony\Component\OptionsResolver\OptionsResolver;
9+
10+
class CountFilter extends AbstractFilter
11+
{
12+
/**
13+
* To ensure sub query alias are unique.
14+
*
15+
* @var int
16+
*/
17+
static $subQueryCounter = 0;
18+
19+
protected function configureOptions(OptionsResolver $resolver)
20+
{
21+
parent::configureOptions($resolver);
22+
23+
$resolver->setDefaults(array(
24+
"condition" => "gte",
25+
"primaryKey" => "id"
26+
));
27+
28+
$resolver->setAllowedValues("condition", ["gt", "gte", "eq", "neq", "lt", "lte"]);
29+
$resolver->setAllowedTypes("primaryKey", ["string"]);
30+
}
31+
32+
public function addOrder(QueryBuilder $qb, $dql, $direction, ClassMetadata $metadata)
33+
{
34+
$alias = str_replace(".", "_", $dql);
35+
36+
// sub query must be placed in select area but as hidden attribute
37+
// the count_$alias can then be used to order
38+
// sub query must be in brackets but not the alias
39+
$subQuery = $this->createSubQuery($qb, $metadata, $dql);
40+
$qb->addSelect("(" . $subQuery->getDQL() . ") AS HIDDEN count_$alias");
41+
$qb->addOrderBy("count_$alias", $direction);
42+
}
43+
44+
public function addExpression(Composite $composite, QueryBuilder $qb, $dql, $search, $key, ClassMetadata $metadata)
45+
{
46+
if (!is_numeric($search)) {
47+
return;
48+
}
49+
50+
$subQuery = $this->createSubQuery($qb, $metadata, $dql);
51+
52+
// sub query must be in brackets
53+
$composite->add($qb->expr()->{$this->options['condition']}("(" . $subQuery->getDQL() . ")", '?' . $key));
54+
$qb->setParameter($key, $search);
55+
}
56+
57+
private function createSubQuery(QueryBuilder $qb, ClassMetadata $metadata, $dql)
58+
{
59+
self::$subQueryCounter++;
60+
61+
// split dql
62+
// e.g. dql is "user.someAttributes". user is the entity short name and someAttributes the property name.
63+
$parts = explode(".", $dql);
64+
$countParts = count($parts);
65+
$property = $parts[$countParts - 1];
66+
$entityShortName = $parts[$countParts - 2];
67+
68+
// get information about (class) names of the ArrayCollection field type
69+
$subQueryEntityClass = $metadata->getAssociationMapping($property)['targetEntity'];
70+
$subQueryMappedBy = $metadata->getAssociationMapping($property)['mappedBy'];
71+
72+
// to ensure sub query alias are unique
73+
$alias = $property . "_" . self::$subQueryCounter;
74+
75+
// create sub query builder with target entity class and unique alias
76+
// COUNT attribute can be managed by option "primaryKey" (default is id)
77+
return $qb->getEntityManager()->getRepository($subQueryEntityClass)->createQueryBuilder($alias)
78+
->select("COUNT($alias.{$this->options['primaryKey']})")
79+
->where("$alias.$subQueryMappedBy = $entityShortName");
80+
}
81+
82+
}

0 commit comments

Comments
 (0)