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
15 changes: 14 additions & 1 deletion UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ If you still need it, please set it up on your own!

Frontend dependencies are handled with NPM. Bower is not used anymore.

A lot of assets that were previously public are handled with NPM and placed in a private `node_modules/` directory.
A lot of assets that were previously public are handled with NPM and placed in a private `node_modules/` directory.
From these dependencies, only the necessary files are exposed publicly through Webpack Encore.

Please check the `src/Resources/public/dist` and the documentation to see the used CSS, JavaScript, images and fonts.
Expand Down Expand Up @@ -173,6 +173,19 @@ When there is no searchable filters, `SearchHandler::search()` returns `null`. P
## Sonata\AdminBundle\Controller\CRUDController
When the service `security.csrf.token_manager` is not available, `getCsrfToken()` returns `null`. Previously, it was returning `false`.

## FilterInterface

The type for argument 4 in `apply()` method has been changed from `array` to `Sonata\AdminBundle\Filter\Model\FilterData`.

Before:
```php
public function apply(ProxyQueryInterface $query, array $filterData): void;
```
After:
```php
public function apply(ProxyQueryInterface $query, FilterData $filterData): void;
```

## FormMapper labels
The form label are now correctly using the label translator strategy for field with `.`
(which won't be replaced by `__`). For instance, with the underscore label strategy, the
Expand Down
20 changes: 11 additions & 9 deletions docs/reference/action_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ If you have the **SonataDoctrineORMAdminBundle** installed you can use the
``CallbackFilter`` filter type e.g. for creating a full text filter::

use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Filter\Model\FilterData;

final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin
{
Expand All @@ -549,17 +550,17 @@ If you have the **SonataDoctrineORMAdminBundle** installed you can use the
]);
}

public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, array $data): void
public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): void
{
if (!$data['value']) {
if (!$data->hasValue()) {
return false;
}

// Use `andWhere` instead of `where` to prevent overriding existing `where` conditions
$query->andWhere($query->expr()->orX(
$query->expr()->like($alias.'.username', $query->expr()->literal('%' . $data['value'] . '%')),
$query->expr()->like($alias.'.firstName', $query->expr()->literal('%' . $data['value'] . '%')),
$query->expr()->like($alias.'.lastName', $query->expr()->literal('%' . $data['value'] . '%'))
$query->expr()->like($alias.'.username', $query->expr()->literal('%' . $data->getValue() . '%')),
$query->expr()->like($alias.'.firstName', $query->expr()->literal('%' . $data->getValue() . '%')),
$query->expr()->like($alias.'.lastName', $query->expr()->literal('%' . $data->getValue() . '%'))
));

return true;
Expand All @@ -571,21 +572,22 @@ The callback function should return a boolean indicating whether it is active.
You can also get the filter type which can be helpful to change the operator
type of your condition(s)::

use Sonata\AdminBundle\Filter\Model\FilterData;
use Sonata\Form\Type\EqualType;

final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin
{
public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, array $data): void
public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): void
{
if (!$data['value']) {
if (!$data->hasValue()) {
return;
}

$operator = $data['type'] == EqualType::TYPE_IS_EQUAL ? '=' : '!=';
$operator = $data->isType(EqualType::TYPE_IS_EQUAL) ? '=' : '!=';

$query
->andWhere($alias.'.username '.$operator.' :username')
->setParameter('username', $data['value'])
->setParameter('username', $data->getValue())
;

return true;
Expand Down
4 changes: 4 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ parameters:
- # This error is made on purpose for php version < 8
message: '#^Method Sonata\\AdminBundle\\Tests\\Fixtures\\Entity\\FooToStringNull\:\:__toString\(\) should return string but returns null\.$#'
path: tests/Fixtures/Entity/FooToStringNull.php
- # It is on purpose because it throws an exception
message: "#^Call to static method Sonata\\\\AdminBundle\\\\Filter\\\\Model\\\\FilterData\\:\\:fromArray\\(\\) on a separate line has no effect\\.$#"
count: 1
path: tests/Filter/Model/FilterDataTest.php
6 changes: 6 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@
<InternalClass errorLevel="suppress"/>
<InternalMethod errorLevel="suppress"/>
<InternalProperty errorLevel="suppress"/>
<InvalidArgument>
<errorLevel type="suppress">
<!-- On purpose for test throwing exceptions -->
<file name="tests/Filter/Model/FilterDataTest.php"/>
</errorLevel>
</InvalidArgument>
</issueHandlers>
</psalm>
6 changes: 2 additions & 4 deletions src/Filter/FilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Sonata\AdminBundle\Filter;

use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Filter\Model\FilterData;

/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
Expand All @@ -24,10 +25,7 @@ interface FilterInterface

public const CONDITION_AND = 'AND';

/**
* @phpstan-param array{type?: int|null, value?: mixed} $filterData
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realised that type could not exists, but not sure if that makes sense.

With the current implementation FilterData::getType() would return null if type is not present in the array.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null and no value should behave the same IMHO.

*/
public function apply(ProxyQueryInterface $query, array $filterData): void;
public function apply(ProxyQueryInterface $query, FilterData $filterData): void;

/**
* @throws \LogicException if the filter is not initialized
Expand Down
106 changes: 106 additions & 0 deletions src/Filter/Model/FilterData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\AdminBundle\Filter\Model;

/**
* @psalm-immutable
*/
final class FilterData
{
/**
* @var ?int
*/
private $type;

/**
* @var mixed
*/
private $value;

/**
* @var bool
*/
private $hasValue;

private function __construct()
{
$this->hasValue = false;
}

/**
* @psalm-pure
*
* @phpstan-param array{type?: int|numeric-string|null, value?: mixed} $data
*/
public static function fromArray(array $data): self
{
$filterData = new self();

if (isset($data['type'])) {
if (!\is_int($data['type']) && (!\is_string($data['type']) || !is_numeric($data['type']))) {
throw new \InvalidArgumentException(sprintf(
'The "type" parameter MUST be of type "integer" or "null", %s given.',
\is_object($data['type']) ? 'instance of "'.\get_class($data['type']).'"' : '"'.\gettype($data['type']).'"'
));
}

$filterData->type = (int) $data['type'];
}

if (\array_key_exists('value', $data)) {
$filterData->value = $data['value'];
$filterData->hasValue = true;
}

return $filterData;
}

/**
* @return mixed
*/
public function getValue()
{
if (!$this->hasValue) {
throw new \LogicException('The FilterData object does not have a value.');
}

return $this->value;
}

/**
* @param mixed $value
*/
public function changeValue($value): self
{
return self::fromArray([
'type' => $this->getType(),
'value' => $value,
]);
}

public function getType(): ?int
{
return $this->type;
}

public function isType(int $type): bool
{
return $this->type === $type;
}

public function hasValue(): bool
{
return $this->hasValue;
}
}
50 changes: 50 additions & 0 deletions src/Form/DataTransformer/FilterDataTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\AdminBundle\Form\DataTransformer;

use Sonata\AdminBundle\Filter\Model\FilterData;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

final class FilterDataTransformer implements DataTransformerInterface
{
public function reverseTransform($value): FilterData
{
if (!\is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
}

return FilterData::fromArray($value);
}

/**
* @param FilterData|null $value
*/
public function transform($value)
{
if (null === $value) {
return null;
}

$data = [
'type' => $value->getType(),
];

if ($value->hasValue()) {
$data['value'] = $value->getValue();
}

return $data;
}
}
12 changes: 2 additions & 10 deletions src/Form/Type/Filter/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Sonata\AdminBundle\Form\Type\Operator\ContainsOperatorType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType as FormChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand All @@ -29,23 +28,16 @@ public function getBlockPrefix(): string
return 'sonata_type_filter_choice';
}

/**
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
public function getParent(): string
{
$builder
->add('type', $options['operator_type'], $options['operator_options'] + ['required' => false])
->add('value', $options['field_type'], $options['field_options'] + ['required' => false]);
return FilterDataType::class;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'field_type' => FormChoiceType::class,
'field_options' => [],
'operator_type' => ContainsOperatorType::class,
'operator_options' => [],
]);
}
}
11 changes: 3 additions & 8 deletions src/Form/Type/Filter/DateRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Sonata\Form\Type\DateRangeType as FormDateRangeType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand All @@ -30,19 +29,15 @@ public function getBlockPrefix(): string
return 'sonata_type_filter_date_range';
}

/**
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
public function getParent(): string
{
$builder
->add('type', DateRangeOperatorType::class, ['required' => false])
->add('value', $options['field_type'], $options['field_options'] + ['required' => false]);
return FilterDataType::class;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'operator_type' => DateRangeOperatorType::class,
'field_type' => FormDateRangeType::class,
'field_options' => [
'field_options' => [
Expand Down
11 changes: 3 additions & 8 deletions src/Form/Type/Filter/DateTimeRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Sonata\Form\Type\DateTimeRangeType as FormDateTimeRangeType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand All @@ -30,19 +29,15 @@ public function getBlockPrefix(): string
return 'sonata_type_filter_datetime_range';
}

/**
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
public function getParent(): string
{
$builder
->add('type', DateRangeOperatorType::class, ['required' => false])
->add('value', $options['field_type'], $options['field_options'] + ['required' => false]);
return FilterDataType::class;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'operator_type' => DateRangeOperatorType::class,
'field_type' => FormDateTimeRangeType::class,
'field_options' => [
'field_options' => ['date_format' => DateTimeType::HTML5_FORMAT],
Expand Down
Loading