diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md new file mode 100644 index 0000000..68b84d2 --- /dev/null +++ b/UPGRADE-4.0.md @@ -0,0 +1,49 @@ +UPGRADE FROM 3.x to 4.0 +======================= + +## Added `Sonata\DatagridBundle\Field\FieldDescriptionInterface` and `Sonata\DatagridBundle\Field\BaseFieldDescription` + +They are similar to `Sonata\AdminBundle\Admin\FieldDescriptionInterface` +and `Sonata\AdminBundle\Admin\BaseFieldDescription` except that + +- `name` is now required to create a new FieldDescription. +- `getFieldMapping` and `setFieldMapping` now use an `FieldMappingInterface`. +- `getAssociationMapping` and `setAssociationMapping` now use an `AssociationMappingInterface`. +- `getParentAssociationMappings` and `setParentAssociationMappings` now use an `AssociationMappingInterface`. +- `FieldDescriptionInterface::getFieldName` were removed +in favor of `FieldMappingInterface::getFieldName`. +- `FieldDescriptionInterface::setFieldName` were removed +in favor of `FieldMappingInterface::setFieldName`. +- `FieldDescriptionInterface::getMappingType` were removed +in favor of `FieldMappingInterface::getMappingType`. +- `FieldDescriptionInterface::setMappingType` were removed +in favor of `FieldMappingInterface::setMappingType`. +- `FieldDescriptionInterface::getLabel` was removed +in favor of `FieldDescriptionInterface::getOption('label')`. +- `FieldDescriptionInterface::getTranslationDomain` was removed +in favor of `FieldDescriptionInterface::getOption('translation_domain')`. +- `FieldDescriptionInterface::isSortable` was removed +in favor of `FieldDescriptionInterface::getOption('sortable')`. +- `FieldDescriptionInterface::getSortFieldMapping` was removed. +- `FieldDescriptionInterface::getSortParentAssociationMapping` was removed. +- All admin-related method was not added. + +`Sonata\AdminBundle\Admin\FieldDescriptionInterface` will extend this interface in the next major. + +## Changed + +- `Sonata\DatagridBundle\ProxyQuery\BaseProxyQuery`: +Moved every DoctrineORM-related methods to `Sonata\DatagridBundle\ProxyQuery\Doctrine\ProxyQuery`. + +- `Sonata\DatagridBundle\Datagrid\DatagridInterface`: +Must now implement `hasDisplayableFilters`, `getSortParameters` and `getPaginationParameters` methods. + +## Removed + +- `Sonata\DatagridBundle\ProxyQuery\Elastica\ProxyQuery`. +- `Sonata\DatagridBundle\Pager\Elastica\Pager`. + +## Deprecated + +- `Sonata\DatagridBundle\ProxyQuery\Doctrine\ProxyQuery`. +- `Sonata\DatagridBundle\Pager\Doctrine\Pager`. diff --git a/composer.json b/composer.json index 9e1d953..c8cfdd9 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,14 @@ ], "require": { "php": "^7.2", + "doctrine/inflector": "^1.4 || ^2.0", "symfony/config": "^3.4 || ^4.4 || ^5.0", "symfony/dependency-injection": "^3.4 || ^4.4 || ^5.0", "symfony/form": "^3.4 || ^4.4 || ^5.0", "symfony/http-kernel": "^3.4 || ^4.4 || ^5.0" }, "require-dev": { - "doctrine/orm": "^2.4", + "doctrine/orm": "^2.7", "symfony/phpunit-bridge": "^5.0" }, "config": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index a7c5d7d..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,17 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Parameter \\$builder of method Sonata\\\\DatagridBundle\\\\Pager\\\\Elastica\\\\Pager\\:\\:create\\(\\) has invalid typehint type Elastica\\\\QueryBuilder\\.$#" - count: 1 - path: src/Pager/Elastica/Pager.php - - - - message: "#^Access to constant OPTION_SIZE on an unknown class Elastica\\\\Search\\.$#" - count: 1 - path: src/ProxyQuery/Elastica/ProxyQuery.php - - - - message: "#^Access to constant OPTION_FROM on an unknown class Elastica\\\\Search\\.$#" - count: 1 - path: src/ProxyQuery/Elastica/ProxyQuery.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ff0da2c..4dc08f9 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: 0 diff --git a/src/Datagrid/Datagrid.php b/src/Datagrid/Datagrid.php index 5316edd..884cf3e 100644 --- a/src/Datagrid/Datagrid.php +++ b/src/Datagrid/Datagrid.php @@ -13,9 +13,12 @@ namespace Sonata\DatagridBundle\Datagrid; +use Sonata\DatagridBundle\Field\FieldDescriptionInterface; use Sonata\DatagridBundle\Filter\FilterInterface; use Sonata\DatagridBundle\Pager\PagerInterface; use Sonata\DatagridBundle\ProxyQuery\ProxyQueryInterface; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormInterface; @@ -55,7 +58,7 @@ final class Datagrid implements DatagridInterface private $formBuilder; /** - * @var FormInterface + * @var FormInterface|null */ private $form; @@ -109,6 +112,15 @@ public function buildPager(): void $this->formBuilder->add('_page', HiddenType::class); $this->formBuilder->add('_per_page', HiddenType::class); + $this->formBuilder->get('_sort_by')->addViewTransformer(new CallbackTransformer( + static function ($value) { + return $value; + }, + static function ($value) { + return $value instanceof FieldDescriptionInterface ? $value->getName() : $value; + } + )); + $this->form = $this->formBuilder->getForm(); $this->form->submit($this->values); @@ -116,12 +128,25 @@ public function buildPager(): void foreach ($this->getFilters() as $name => $filter) { $this->values[$name] = $this->values[$name] ?? null; - $filter->apply($this->query, $data[$filter->getFormName()] ?? null); + + $filterFormName = $filter->getFormName(); + if (isset($this->values[$filterFormName]['value']) && '' !== $this->values[$filterFormName]['value']) { + $filter->apply($this->query, $data[$filterFormName]); + } } if (isset($this->values['_sort_by'])) { - $this->query->setSortBy($this->values['_sort_by']); - $this->query->setSortOrder($this->values['_sort_order'] ?? null); + $sortBy = $this->values['_sort_by']; + if (!$sortBy instanceof FieldDescriptionInterface) { + throw new UnexpectedTypeException($this->values['_sort_by'], FieldDescriptionInterface::class); + } + + if (false !== $sortBy->getOption('sortable', false)) { + $this->values['_sort_order'] = $this->values['_sort_order'] ?? 'ASC'; + + $this->query->setSortBy($this->values['_sort_by']); + $this->query->setSortOrder($this->values['_sort_order']); + } } $this->pager->setMaxPerPage($this->values['_per_page'] ?? 25); @@ -189,6 +214,18 @@ public function hasActiveFilters(): bool return false; } + public function hasDisplayableFilters(): bool + { + foreach ($this->filters as $name => $filter) { + $showFilter = $filter->getOption('show_filter', null); + if (($filter->isActive() && null === $showFilter) || (true === $showFilter)) { + return true; + } + } + + return false; + } + public function getQuery(): ProxyQueryInterface { return $this->query; @@ -200,4 +237,49 @@ public function getForm(): FormInterface return $this->form; } + + public function getSortParameters(FieldDescriptionInterface $fieldDescription): array + { + $values = $this->getValues(); + + if ($this->isFieldAlreadySorted($fieldDescription)) { + if ('ASC' === $values['_sort_order']) { + $values['_sort_order'] = 'DESC'; + } else { + $values['_sort_order'] = 'ASC'; + } + } else { + $values['_sort_order'] = 'ASC'; + } + + $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) + ? $fieldDescription->getOption('sortable') + : $fieldDescription->getName(); + + return ['filter' => $values]; + } + + public function getPaginationParameters(int $page): array + { + $values = $this->getValues(); + + if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) { + $values['_sort_by'] = $values['_sort_by']->getName(); + } + $values['_page'] = $page; + + return ['filter' => $values]; + } + + private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription): bool + { + $values = $this->getValues(); + + if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) { + return false; + } + + return $values['_sort_by']->getName() === $fieldDescription->getName() + || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable'); + } } diff --git a/src/Datagrid/DatagridInterface.php b/src/Datagrid/DatagridInterface.php index cd9f9a0..7325a84 100644 --- a/src/Datagrid/DatagridInterface.php +++ b/src/Datagrid/DatagridInterface.php @@ -13,6 +13,7 @@ namespace Sonata\DatagridBundle\Datagrid; +use Sonata\DatagridBundle\Field\FieldDescriptionInterface; use Sonata\DatagridBundle\Filter\FilterInterface; use Sonata\DatagridBundle\Pager\PagerInterface; use Sonata\DatagridBundle\ProxyQuery\ProxyQueryInterface; @@ -50,4 +51,10 @@ public function hasFilter(string $name): bool; public function removeFilter(string $name): void; public function hasActiveFilters(): bool; + + public function hasDisplayableFilters(): bool; + + public function getSortParameters(FieldDescriptionInterface $fieldDescription): array; + + public function getPaginationParameters(int $page): array; } diff --git a/src/Exception/NoValueException.php b/src/Exception/NoValueException.php new file mode 100644 index 0000000..658df12 --- /dev/null +++ b/src/Exception/NoValueException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Exception; + +/** + * @author Thomas Rabaix + */ +class NoValueException extends \Exception +{ +} diff --git a/src/Field/BaseFieldDescription.php b/src/Field/BaseFieldDescription.php new file mode 100644 index 0000000..9cedfea --- /dev/null +++ b/src/Field/BaseFieldDescription.php @@ -0,0 +1,317 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Field; + +use Doctrine\Inflector\InflectorFactory; +use Sonata\DatagridBundle\Exception\NoValueException; +use Sonata\DatagridBundle\Mapping\AssociationMappingInterface; +use Sonata\DatagridBundle\Mapping\FieldMappingInterface; + +/** + * @author Thomas Rabaix + */ +abstract class BaseFieldDescription implements FieldDescriptionInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var string|null + */ + private $type; + + /** + * @var AssociationMappingInterface|null + */ + private $associationMapping; + + /** + * @var FieldMappingInterface|null + */ + private $fieldMapping; + + /** + * @var AssociationMappingInterface[] + */ + private $parentAssociationMappings = []; + + /** + * @var string + */ + private $template; + + /** + * @var array + */ + private $options = []; + + /** + * @var array[] + */ + private static $fieldGetters = []; + + public function __construct(string $name) + { + $this->setName($name); + } + + final public function setName(string $name): void + { + $this->name = $name; + } + + final public function getName(): string + { + return $this->name; + } + + final public function setTemplate(string $template): void + { + $this->template = $template; + } + + final public function getTemplate(): ?string + { + return $this->template; + } + + final public function setType(string $type): void + { + $this->type = $type; + } + + final public function getType(): ?string + { + return $this->type; + } + + /** + * @param mixed $default + * + * @return mixed + */ + final public function getOption(string $name, $default = null) + { + return isset($this->options[$name]) ? $this->options[$name] : $default; + } + + /** + * @param mixed $value + */ + final public function setOption(string $name, $value): void + { + $this->options[$name] = $value; + } + + final public function setOptions(array $options): void + { + // set the type if provided + if (isset($options['type'])) { + $this->setType($options['type']); + unset($options['type']); + } + + // set the template if provided + if (isset($options['template'])) { + $this->setTemplate($options['template']); + unset($options['template']); + } + + $this->options = $options; + } + + final public function getOptions(): array + { + return $this->options; + } + + final public function mergeOption(string $name, array $options = []): void + { + if (!isset($this->options[$name])) { + $this->options[$name] = []; + } + + if (!\is_array($this->options[$name])) { + throw new \RuntimeException(sprintf('The key `%s` does not point to an array value', $name)); + } + + $this->options[$name] = array_merge($this->options[$name], $options); + } + + final public function mergeOptions(array $options = []): void + { + foreach ($options as $name => $option) { + if (\is_array($option)) { + $this->mergeOption($name, $option); + } else { + $this->setOption($name, $option); + } + } + } + + final public function getAssociationMapping(): ?AssociationMappingInterface + { + return $this->associationMapping; + } + + final public function setAssociationMapping(?AssociationMappingInterface $associationMapping): void + { + $this->associationMapping = $associationMapping; + } + + final public function getFieldMapping(): ?FieldMappingInterface + { + return $this->fieldMapping; + } + + final public function setFieldMapping(?FieldMappingInterface $fieldMapping): void + { + $this->fieldMapping = $fieldMapping; + } + + final public function getParentAssociationMappings(): array + { + return $this->parentAssociationMappings; + } + + /** + * @param AssociationMappingInterface[] $parentAssociationMappings + */ + final public function setParentAssociationMappings(array $parentAssociationMappings): void + { + $this->parentAssociationMappings = $parentAssociationMappings; + } + + final public function isVirtual(): bool + { + return false !== $this->getOption('virtual_field', false); + } + + /** + * @throws NoValueException + * + * @return mixed + */ + final public function getFieldValue(?object $object, ?string $fieldName) + { + if ($this->isVirtual() || null === $object) { + return null; + } + + $getters = []; + $parameters = []; + + // prefer method name given in the code option + if ($this->getOption('code')) { + $getters[] = $this->getOption('code'); + } + // parameters for the method given in the code option + if ($this->getOption('parameters')) { + $parameters = $this->getOption('parameters'); + } + + if (null !== $fieldName && '' !== $fieldName) { + if ($this->hasCachedFieldGetter($object, $fieldName)) { + return $this->callCachedGetter($object, $fieldName, $parameters); + } + + $camelizedFieldName = InflectorFactory::create()->build()->classify($fieldName); + + $getters[] = 'get'.$camelizedFieldName; + $getters[] = 'is'.$camelizedFieldName; + $getters[] = 'has'.$camelizedFieldName; + } + + foreach ($getters as $getter) { + if (method_exists($object, $getter) && \is_callable([$object, $getter])) { + $this->cacheFieldGetter($object, $fieldName, 'getter', $getter); + + return $object->{$getter}(...$parameters); + } + } + + if (null !== $fieldName) { + if (method_exists($object, '__call')) { + $this->cacheFieldGetter($object, $fieldName, 'call'); + + return $object->{$fieldName}(...$parameters); + } + + if ('' !== $fieldName && isset($object->{$fieldName})) { + $this->cacheFieldGetter($object, $fieldName, 'var'); + + return $object->{$fieldName}; + } + } + + throw new NoValueException(sprintf( + 'Neither the property "%s" nor one of the methods "%s()" exist and have public access in class "%s".', + $this->getName(), + implode('()", "', $getters), + \get_class($object) + )); + } + + private function getFieldGetterKey(object $object, ?string $fieldName): ?string + { + if (null === $fieldName) { + return null; + } + + $components = [\get_class($object), $fieldName]; + + $code = $this->getOption('code'); + if (\is_string($code) && '' !== $code) { + $components[] = $code; + } + + return implode('-', $components); + } + + private function hasCachedFieldGetter(object $object, string $fieldName): bool + { + return isset( + self::$fieldGetters[$this->getFieldGetterKey($object, $fieldName)] + ); + } + + private function callCachedGetter(object $object, string $fieldName, array $parameters = []) + { + $getterKey = $this->getFieldGetterKey($object, $fieldName); + + if ('getter' === self::$fieldGetters[$getterKey]['method']) { + return $object->{self::$fieldGetters[$getterKey]['getter']}(...$parameters); + } + + if ('call' === self::$fieldGetters[$getterKey]['method']) { + return $object->__call($fieldName, $parameters); + } + + return $object->{$fieldName}; + } + + private function cacheFieldGetter(object $object, ?string $fieldName, string $method, ?string $getter = null): void + { + $getterKey = $this->getFieldGetterKey($object, $fieldName); + + if (null !== $getterKey) { + self::$fieldGetters[$getterKey] = ['method' => $method]; + if (null !== $getter) { + self::$fieldGetters[$getterKey]['getter'] = $getter; + } + } + } +} diff --git a/src/Field/FieldDescriptionInterface.php b/src/Field/FieldDescriptionInterface.php new file mode 100644 index 0000000..f8d423e --- /dev/null +++ b/src/Field/FieldDescriptionInterface.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Field; + +use Sonata\DatagridBundle\Exception\NoValueException; +use Sonata\DatagridBundle\Mapping\AssociationMappingInterface; +use Sonata\DatagridBundle\Mapping\FieldMappingInterface; + +/** + * @author Thomas Rabaix + */ +interface FieldDescriptionInterface +{ + /** + * Set the name, used as a form label or table header. + */ + public function setName(string $name): void; + + public function getName(): string; + + /** + * Set the template, used to render the field. + */ + public function setTemplate(string $template): void; + + public function getTemplate(): ?string; + + /** + * Set the type, this is a mandatory field as it used to select the correct template + * or the logic associated to the current FieldDescription object. + */ + public function setType(string $type): void; + + public function getType(): ?string; + + /** + * @param mixed $default + * + * @return mixed + */ + public function getOption(string $name, $default = null); + + /** + * @param mixed $value + */ + public function setOption(string $name, $value): void; + + /** + * Define the options value, if the options array contains the reserved keywords + * - type + * - template. + * + * Then the value are copied across to the related property value + */ + public function setOptions(array $options): void; + + public function getOptions(): array; + + /** + * @throws \RuntimeException + */ + public function mergeOption(string $name, array $options = []): void; + + public function mergeOptions(array $options = []): void; + + public function setAssociationMapping(?AssociationMappingInterface $associationMapping): void; + + public function getAssociationMapping(): ?AssociationMappingInterface; + + public function setFieldMapping(?FieldMappingInterface $fieldMapping): void; + + public function getFieldMapping(): ?FieldMappingInterface; + + /** + * @param AssociationMappingInterface[] $parentAssociationMappings + */ + public function setParentAssociationMappings(array $parentAssociationMappings): void; + + /** + * @return AssociationMappingInterface[] + */ + public function getParentAssociationMappings(): array; + + public function isIdentifier(): bool; + + /** + * @throws NoValueException + * + * @return mixed + */ + public function getValue(?object $object); + + /** + * @throws NoValueException + * + * @return mixed + */ + public function getFieldValue(?object $object, string $fieldName); +} diff --git a/src/Filter/FilterFactoryInterface.php b/src/Filter/FilterFactoryInterface.php index 1a6cde4..bd4561d 100644 --- a/src/Filter/FilterFactoryInterface.php +++ b/src/Filter/FilterFactoryInterface.php @@ -15,8 +15,5 @@ interface FilterFactoryInterface { - /** - * @return mixed - */ - public function create(string $name, string $type, array $options = []); + public function create(string $name, string $type, array $options = []): FilterInterface; } diff --git a/src/Mapping/AssociationMappingInterface.php b/src/Mapping/AssociationMappingInterface.php new file mode 100644 index 0000000..cdfd59e --- /dev/null +++ b/src/Mapping/AssociationMappingInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Mapping; + +interface AssociationMappingInterface extends FieldMappingInterface +{ + public const ONE_TO_ONE = 'association_mapping_type_one_to_one'; + public const MANY_TO_ONE = 'association_mapping_type_many_to_one'; + public const ONE_TO_MANY = 'association_mapping_type_one_to_many'; + public const MANY_TO_MANY = 'association_mapping_type_many_to_many'; + + public function getMappingType(): ?string; + + public function getTargetModel(): ?string; +} diff --git a/src/Mapping/FieldMappingInterface.php b/src/Mapping/FieldMappingInterface.php new file mode 100644 index 0000000..e14bbd2 --- /dev/null +++ b/src/Mapping/FieldMappingInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Mapping; + +interface FieldMappingInterface +{ + public function getFieldName(): ?string; +} diff --git a/src/Pager/Doctrine/Pager.php b/src/Pager/Doctrine/Pager.php index 109264e..e3bbf30 100644 --- a/src/Pager/Doctrine/Pager.php +++ b/src/Pager/Doctrine/Pager.php @@ -13,7 +13,6 @@ namespace Sonata\DatagridBundle\Pager\Doctrine; -use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Sonata\DatagridBundle\Pager\BasePager; use Sonata\DatagridBundle\Pager\PagerInterface; @@ -22,6 +21,14 @@ /** * @author Jonathan H. Wage + * + * TODO: + * This bundle should not rely on Doctrine\ORM. You should use the DoctrineORMAdminBundle Pager + * instead because multiple bugfix/new feature was not added to the DatagridBundle Pager but + * the DoctrineORMAdminBundle require SonataAdminBundle which lead to a lot of requirements for nothing. + * We should split DoctrineORMAdminBundle to provide a DoctrineORMDatagridBundle. + * + * @deprecated prefer using the DoctrineORMAdminBundle Pager if possible */ final class Pager extends BasePager { @@ -48,9 +55,9 @@ public function computeNbResult(): int return (int) $countQuery->getQuery()->getSingleScalarResult(); } - public function getResults($hydrationMode = Query::HYDRATE_OBJECT): ?array + public function getResults(): ?array { - return $this->getQuery()->execute([], $hydrationMode); + return $this->getQuery()->execute(); } public function init(): void diff --git a/src/Pager/Elastica/Pager.php b/src/Pager/Elastica/Pager.php deleted file mode 100644 index 7280385..0000000 --- a/src/Pager/Elastica/Pager.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Sonata\DatagridBundle\Pager\Elastica; - -use Elastica\QueryBuilder; -use Sonata\DatagridBundle\Pager\BasePager; -use Sonata\DatagridBundle\Pager\PagerInterface; -use Sonata\DatagridBundle\ProxyQuery\Elastica\ProxyQuery; -use Sonata\DatagridBundle\ProxyQuery\ProxyQueryInterface; - -/** - * @author Vincent Composieux - */ -final class Pager extends BasePager -{ - public function computeNbResult(): int - { - $countQuery = clone $this->getQuery(); - $countQuery->execute(); - - return $countQuery->getResults()->getTotalHits(); - } - - public function getResults(): ?array - { - return $this->getQuery()->execute(); - } - - public function init(): void - { - $this->resetIterator(); - - $query = $this->getQuery(); - - \assert($query instanceof ProxyQueryInterface); - - $query->setMaxResults($this->getMaxPerPage()); - - $this->setNbResults($this->computeNbResult()); - - if (\count($this->getParameters()) > 0) { - $query->setParameters($this->getParameters()); - } - - if (0 === $this->getPage() || 0 === $this->getMaxPerPage() || 0 === $this->getNbResults()) { - $this->setLastPage(0); - } else { - $offset = ($this->getPage() - 1) * $this->getMaxPerPage(); - - $this->setLastPage((int) ceil($this->getNbResults() / $this->getMaxPerPage())); - - $query->setFirstResult($offset); - $query->setMaxResults($this->getMaxPerPage()); - } - } - - /** - * Builds a pager for a given query builder. - */ - public static function create(QueryBuilder $builder, int $limit, int $page): PagerInterface - { - $pager = new self($limit); - $pager->setQuery(new ProxyQuery($builder)); - $pager->setPage($page); - $pager->init(); - - return $pager; - } -} diff --git a/src/ProxyQuery/BaseProxyQuery.php b/src/ProxyQuery/BaseProxyQuery.php index 81147a3..9b88294 100644 --- a/src/ProxyQuery/BaseProxyQuery.php +++ b/src/ProxyQuery/BaseProxyQuery.php @@ -13,29 +13,19 @@ namespace Sonata\DatagridBundle\ProxyQuery; -use Doctrine\ORM\QueryBuilder; +use Sonata\DatagridBundle\Field\FieldDescriptionInterface; abstract class BaseProxyQuery implements ProxyQueryInterface { /** - * @var QueryBuilder + * @var FieldDescriptionInterface|null */ - protected $queryBuilder; + private $sortBy; /** - * @var array + * @var string|null */ - protected $results = []; - - /** - * @var array - */ - private $sortBy = []; - - /** - * @var array - */ - private $sortOrder = []; + private $sortOrder; /** * @var int|null @@ -47,53 +37,31 @@ abstract class BaseProxyQuery implements ProxyQueryInterface */ private $maxResults; - public function __construct(QueryBuilder $queryBuilder) - { - $this->queryBuilder = $queryBuilder; - } - - public function __clone() - { - $this->queryBuilder = clone $this->queryBuilder; - } - - public function __call(string $name, array $args) - { - return \call_user_func_array([$this->queryBuilder, $name], $args); - } - /** - * @param mixed $sortBy + * @var int */ - public function setSortBy($sortBy): ProxyQueryInterface + private $uniqueParameterId = 0; + + public function setSortBy(?FieldDescriptionInterface $sortBy): ProxyQueryInterface { $this->sortBy = $sortBy; return $this; } - /** - * @return mixed - */ - public function getSortBy() + public function getSortBy(): ?FieldDescriptionInterface { return $this->sortBy; } - /** - * @param mixed $sortOrder - */ - public function setSortOrder($sortOrder): ProxyQueryInterface + public function setSortOrder(?string $sortOrder): ProxyQueryInterface { $this->sortOrder = $sortOrder; return $this; } - /** - * @return mixed - */ - public function getSortOrder() + public function getSortOrder(): ?string { return $this->sortOrder; } @@ -122,16 +90,8 @@ public function getMaxResults(): ?int return $this->maxResults; } - /** - * @return mixed - */ - public function getQueryBuilder() - { - return $this->queryBuilder; - } - - public function getResults(): array + public function getUniqueParameterId(): int { - return $this->results; + return $this->uniqueParameterId++; } } diff --git a/src/ProxyQuery/Doctrine/ProxyQuery.php b/src/ProxyQuery/Doctrine/ProxyQuery.php index 583a3a7..e9b4303 100644 --- a/src/ProxyQuery/Doctrine/ProxyQuery.php +++ b/src/ProxyQuery/Doctrine/ProxyQuery.php @@ -15,10 +15,88 @@ use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; +use Sonata\DatagridBundle\Mapping\AssociationMappingInterface; use Sonata\DatagridBundle\ProxyQuery\BaseProxyQuery; +/** + * TODO: + * This bundle should not rely on Doctrine\ORM. You should use the DoctrineORMAdminBundle ProxyQuery + * instead because multiple bugfix/new feature was not added to the DatagridBundle ProxyQuery but + * the DoctrineORMAdminBundle require SonataAdminBundle which lead to a lot of requirements for nothing. + * We should split DoctrineORMAdminBundle to provide a DoctrineORMDatagridBundle. + * + * @deprecated prefer using the DoctrineORMAdminBundle ProxyQuery if possible + */ final class ProxyQuery extends BaseProxyQuery { + /** + * @var QueryBuilder + */ + private $queryBuilder; + + /** + * @var array + */ + private $entityJoinAliases = []; + + public function __construct(QueryBuilder $queryBuilder) + { + $this->queryBuilder = $queryBuilder; + } + + public function __clone() + { + $this->queryBuilder = clone $this->queryBuilder; + } + + public function __call(string $name, array $args) + { + return \call_user_func_array([$this->queryBuilder, $name], $args); + } + + /** + * @param AssociationMappingInterface[] $associationMappings + */ + public function entityJoin(array $associationMappings): string + { + $alias = current($this->queryBuilder->getRootAliases()); + + $newAlias = 's'; + + $joinedEntities = $this->queryBuilder->getDQLPart('join'); + + foreach ($associationMappings as $associationMapping) { + // Do not add left join to already joined entities with custom query + foreach ($joinedEntities as $joinExprList) { + foreach ($joinExprList as $joinExpr) { + $newAliasTmp = $joinExpr->getAlias(); + + if (sprintf('%s.%s', $alias, $associationMapping->getFieldName()) === $joinExpr->getJoin()) { + $this->entityJoinAliases[] = $newAliasTmp; + $alias = $newAliasTmp; + + continue 3; + } + } + } + + $newAlias .= '_'.$associationMapping->getFieldName(); + if (!\in_array($newAlias, $this->entityJoinAliases, true)) { + $this->entityJoinAliases[] = $newAlias; + $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping->getFieldName()), $newAlias); + } + + $alias = $newAlias; + } + + return $alias; + } + + public function getQueryBuilder(): QueryBuilder + { + return $this->queryBuilder; + } + public function execute(array $params = [], ?int $hydrationMode = null) { $this->queryBuilder->setMaxResults($this->getMaxResults()); @@ -28,6 +106,9 @@ public function execute(array $params = [], ?int $hydrationMode = null) $sortOrder = $this->getSortOrder(); if ($sortBy && $sortOrder) { + $alias = $this->entityJoin($sortBy->getParentAssociationMappings()); + $sortBy = $alias.'.'.$sortBy->getFieldMapping()->getFieldName(); + $rootAliases = $this->queryBuilder->getRootAliases(); $rootAlias = $rootAliases[0]; $sortBy = sprintf('%s.%s', $rootAlias, $sortBy); @@ -41,7 +122,7 @@ public function execute(array $params = [], ?int $hydrationMode = null) /** * Generates new QueryBuilder for Postgresql or Oracle if necessary. */ - public function preserveSqlOrdering(QueryBuilder $queryBuilder): QueryBuilder + private function preserveSqlOrdering(QueryBuilder $queryBuilder): QueryBuilder { $rootAliases = $queryBuilder->getRootAliases(); $rootAlias = $rootAliases[0]; @@ -53,7 +134,9 @@ public function preserveSqlOrdering(QueryBuilder $queryBuilder): QueryBuilder // todo : check how doctrine behave, potential SQL injection here ... if ($this->getSortBy()) { - $sortBy = $this->getSortBy(); + $alias = $this->entityJoin($this->getSortBy()->getParentAssociationMappings()); + $sortBy = $alias.'.'.$this->getSortBy()->getFieldMapping()->getFieldName(); + if (false === strpos($sortBy, '.')) { // add the current alias $sortBy = $rootAlias.'.'.$sortBy; diff --git a/src/ProxyQuery/Elastica/ProxyQuery.php b/src/ProxyQuery/Elastica/ProxyQuery.php deleted file mode 100644 index 649a735..0000000 --- a/src/ProxyQuery/Elastica/ProxyQuery.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Sonata\DatagridBundle\ProxyQuery\Elastica; - -use Elastica\Search; -use Sonata\DatagridBundle\ProxyQuery\BaseProxyQuery; - -final class ProxyQuery extends BaseProxyQuery -{ - public function execute(array $params = [], ?int $hydrationMode = null) - { - $query = $this->queryBuilder->getQuery(); - - $sortBy = $this->getSortBy(); - $sortOrder = $this->getSortOrder(); - - if ($sortBy && $sortOrder) { - $query->setSort([$sortBy => ['order' => $sortOrder]]); - } - - $this->results = $this->queryBuilder - ->getRepository() - ->createPaginatorAdapter( - $query, - [ - Search::OPTION_SIZE => $this->getMaxResults(), - Search::OPTION_FROM => $this->getFirstResult(), - ] - ); - - return $this->results->getResults($this->getFirstResult(), $this->getMaxResults())->toArray(); - } -} diff --git a/src/ProxyQuery/ProxyQueryInterface.php b/src/ProxyQuery/ProxyQueryInterface.php index 394f3e6..cda496b 100644 --- a/src/ProxyQuery/ProxyQueryInterface.php +++ b/src/ProxyQuery/ProxyQueryInterface.php @@ -13,6 +13,9 @@ namespace Sonata\DatagridBundle\ProxyQuery; +use Sonata\DatagridBundle\Field\FieldDescriptionInterface; +use Sonata\DatagridBundle\Mapping\AssociationMappingInterface; + /** * Interface used by the Datagrid to build the query. */ @@ -26,27 +29,15 @@ public function __call(string $name, array $args); /** * @return mixed */ - public function execute(array $params = [], ?int $hydrationMode = null); + public function execute(); - /** - * @param mixed $sortBy - */ - public function setSortBy($sortBy): self; + public function setSortBy(?FieldDescriptionInterface $sortBy): self; - /** - * @return mixed - */ - public function getSortBy(); + public function getSortBy(): ?FieldDescriptionInterface; - /** - * @param mixed $sortOrder - */ - public function setSortOrder($sortOrder): self; + public function setSortOrder(?string $sortOrder): self; - /** - * @return mixed - */ - public function getSortOrder(); + public function getSortOrder(): ?string; public function setFirstResult(?int $firstResult): self; @@ -56,5 +47,10 @@ public function setMaxResults(?int $maxResults): self; public function getMaxResults(): ?int; - public function getResults(): array; + public function getUniqueParameterId(): int; + + /** + * @param AssociationMappingInterface[] $associationMappings + */ + public function entityJoin(array $associationMappings): string; } diff --git a/tests/Datagrid/DatagridTest.php b/tests/Datagrid/DatagridTest.php index 913ba48..1ccfea8 100644 --- a/tests/Datagrid/DatagridTest.php +++ b/tests/Datagrid/DatagridTest.php @@ -15,18 +15,16 @@ use PHPUnit\Framework\TestCase; use Sonata\DatagridBundle\Datagrid\Datagrid; +use Sonata\DatagridBundle\Field\FieldDescriptionInterface; use Sonata\DatagridBundle\Filter\FilterInterface; use Sonata\DatagridBundle\Pager\PagerInterface; use Sonata\DatagridBundle\ProxyQuery\ProxyQueryInterface; +use Sonata\DatagridBundle\Tests\Fixtures\Field\FieldDescription; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormFactoryInterface; -class TestEntity -{ -} - /** * @author Andrej Hudec */ @@ -79,7 +77,7 @@ protected function setUp(): void $this->formBuilder ->method('add') ->willReturnCallback(function ($name, $type, $options) use ($eventDispatcher, $formFactory): void { - $this->formTypes[$name] = new FormBuilder($name, TestEntity::class, $eventDispatcher, $formFactory, $options); + $this->formTypes[$name] = new FormBuilder($name, \stdClass::class, $eventDispatcher, $formFactory, $options); }); $form = $this->createMock(Form::class); @@ -291,8 +289,9 @@ public function testBuildPager(): void public function testBuildPagerWithSortBy(): void { + $fieldDescription = new FieldDescription('name'); $this->datagrid = new Datagrid($this->query, $this->pager, $this->formBuilder, [ - '_sort_by' => 'name', + '_sort_by' => $fieldDescription, ]); $filter = $this->createMock(FilterInterface::class); @@ -313,7 +312,7 @@ public function testBuildPagerWithSortBy(): void $this->datagrid->buildPager(); - $this->assertSame(['_sort_by' => 'name', 'foo' => null], $this->datagrid->getValues()); + $this->assertSame(['_sort_by' => $fieldDescription, 'foo' => null], $this->datagrid->getValues()); $this->assertInstanceOf(FormBuilder::class, $this->formBuilder->get('fooFormName')); $this->assertSame(['bar' => 'baz'], $this->formBuilder->get('fooFormName')->getOptions()); $this->assertInstanceOf(FormBuilder::class, $this->formBuilder->get('_sort_by')); @@ -321,4 +320,70 @@ public function testBuildPagerWithSortBy(): void $this->assertInstanceOf(FormBuilder::class, $this->formBuilder->get('_page')); $this->assertInstanceOf(FormBuilder::class, $this->formBuilder->get('_per_page')); } + + public function testSortParameters(): void + { + $field1 = $this->createMock(FieldDescriptionInterface::class); + $field1->method('getName')->willReturn('field1'); + + $field2 = $this->createMock(FieldDescriptionInterface::class); + $field2->method('getName')->willReturn('field2'); + + $field3 = $this->createMock(FieldDescriptionInterface::class); + $field3->method('getName')->willReturn('field3'); + $field3->method('getOption')->with('sortable')->willReturn('field3sortBy'); + + $this->datagrid = new Datagrid( + $this->query, + $this->pager, + $this->formBuilder, + ['_sort_by' => $field1, '_sort_order' => 'ASC'] + ); + + $parameters = $this->datagrid->getSortParameters($field1); + + $this->assertSame('DESC', $parameters['filter']['_sort_order']); + $this->assertSame('field1', $parameters['filter']['_sort_by']); + + $parameters = $this->datagrid->getSortParameters($field2); + + $this->assertSame('ASC', $parameters['filter']['_sort_order']); + $this->assertSame('field2', $parameters['filter']['_sort_by']); + + $parameters = $this->datagrid->getSortParameters($field3); + + $this->assertSame('ASC', $parameters['filter']['_sort_order']); + $this->assertSame('field3sortBy', $parameters['filter']['_sort_by']); + + $this->datagrid = new Datagrid( + $this->query, + $this->pager, + $this->formBuilder, + ['_sort_by' => $field3, '_sort_order' => 'ASC'] + ); + + $parameters = $this->datagrid->getSortParameters($field3); + + $this->assertSame('DESC', $parameters['filter']['_sort_order']); + $this->assertSame('field3sortBy', $parameters['filter']['_sort_by']); + } + + public function testGetPaginationParameters(): void + { + $field = $this->createMock(FieldDescriptionInterface::class); + + $this->datagrid = new Datagrid( + $this->query, + $this->pager, + $this->formBuilder, + ['_sort_by' => $field, '_sort_order' => 'ASC'] + ); + + $field->expects($this->once())->method('getName')->willReturn($name = 'test'); + + $result = $this->datagrid->getPaginationParameters($page = 5); + + $this->assertSame($page, $result['filter']['_page']); + $this->assertSame($name, $result['filter']['_sort_by']); + } } diff --git a/tests/Field/FieldDescriptionTest.php b/tests/Field/FieldDescriptionTest.php new file mode 100644 index 0000000..c1ddecf --- /dev/null +++ b/tests/Field/FieldDescriptionTest.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Field; + +use PHPUnit\Framework\TestCase; +use Sonata\DatagridBundle\Exception\NoValueException; +use Sonata\DatagridBundle\Tests\Fixtures\Entity\Foo; +use Sonata\DatagridBundle\Tests\Fixtures\Entity\FooBoolean; +use Sonata\DatagridBundle\Tests\Fixtures\Entity\FooCall; +use Sonata\DatagridBundle\Tests\Fixtures\Field\FieldDescription; + +class FieldDescriptionTest extends TestCase +{ + public function testSetName(): void + { + $description = new FieldDescription('foo'); + + $this->assertSame('foo', $description->getName()); + } + + public function testOptions(): void + { + $description = new FieldDescription('foo'); + $description->setOption('foo', 'bar'); + + $this->assertNull($description->getOption('bar')); + $this->assertSame('bar', $description->getOption('foo')); + + $description->mergeOptions(['settings' => ['value_1', 'value_2']]); + $description->mergeOptions(['settings' => ['value_1', 'value_3']]); + + $this->assertSame(['value_1', 'value_2', 'value_1', 'value_3'], $description->getOption('settings')); + + $description->mergeOption('settings', ['value_4']); + $this->assertSame(['value_1', 'value_2', 'value_1', 'value_3', 'value_4'], $description->getOption('settings')); + + $description->mergeOption('bar', ['hello']); + + $this->assertCount(1, $description->getOption('bar')); + + $description->setOptions(['boolean' => true]); + $description->mergeOptions(['boolean' => false]); + + $this->assertFalse($description->getOption('boolean')); + + $this->assertNull($description->getTemplate()); + $description->setOptions(['type' => 'integer', 'template' => 'foo.twig.html']); + + $this->assertSame('integer', $description->getType()); + $this->assertSame('foo.twig.html', $description->getTemplate()); + + $this->assertCount(0, $description->getOptions()); + } + + public function testGetValue(): void + { + $description = new FieldDescription('foo'); + $description->setOption('code', 'getFoo'); + + $mock = $this->getMockBuilder(\stdClass::class) + ->setMethods(['getFoo']) + ->getMock(); + $mock->expects($this->once())->method('getFoo')->willReturn(42); + + $this->assertSame(42, $description->getFieldValue($mock, 'fake')); + + /* + * Test with One parameter int + */ + $arg1 = 38; + $oneParameter = [$arg1]; + $description1 = new FieldDescription('foo'); + $description1->setOption('code', 'getWithOneParameter'); + $description1->setOption('parameters', $oneParameter); + + $mock1 = $this->getMockBuilder(\stdClass::class) + ->setMethods(['getWithOneParameter']) + ->getMock(); + $returnValue1 = $arg1 + 2; + $mock1->expects($this->once())->method('getWithOneParameter')->with($this->equalTo($arg1))->willReturn($returnValue1); + + $this->assertSame(40, $description1->getFieldValue($mock1, 'fake')); + + /* + * Test with Two parameters int + */ + $arg2 = 4; + $twoParameters = [$arg1, $arg2]; + $description2 = new FieldDescription('foo'); + $description2->setOption('code', 'getWithTwoParameters'); + $description2->setOption('parameters', $twoParameters); + + $mock2 = $this->getMockBuilder(\stdClass::class) + ->setMethods(['getWithTwoParameters']) + ->getMock(); + $returnValue2 = $arg1 + $arg2; + $mock2->method('getWithTwoParameters')->with($this->equalTo($arg1), $this->equalTo($arg2))->willReturn($returnValue2); + $this->assertSame(42, $description2->getFieldValue($mock2, 'fake')); + + /* + * Test with underscored attribute name + */ + foreach (['getFake', 'isFake', 'hasFake'] as $method) { + $description3 = new FieldDescription('foo'); + $mock3 = $this->getMockBuilder(\stdClass::class) + ->setMethods([$method]) + ->getMock(); + + $mock3->expects($this->once())->method($method)->willReturn(42); + $this->assertSame(42, $description3->getFieldValue($mock3, '_fake')); + } + + $mock4 = $this->getMockBuilder('MockedTestObject') + ->setMethods(['myMethod']) + ->getMock(); + $mock4->expects($this->once()) + ->method('myMethod') + ->willReturn('myMethodValue'); + + $description4 = new FieldDescription('foo'); + $description4->setOption('code', 'myMethod'); + + $this->assertSame($description4->getFieldValue($mock4, null), 'myMethodValue'); + } + + public function testGetValueNoValueException(): void + { + $this->expectException(NoValueException::class); + + $description = new FieldDescription('foo'); + $mock = $this->getMockBuilder(\stdClass::class)->setMethods(['getFoo'])->getMock(); + + $description->getFieldValue($mock, 'fake'); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetVirtualValue(): void + { + $description = new FieldDescription('foo'); + $mock = $this->getMockBuilder(\stdClass::class)->setMethods(['getFoo'])->getMock(); + + $description->setOption('virtual_field', true); + $description->getFieldValue($mock, 'fake'); + } + + public function testExceptionOnNonArrayOption(): void + { + $this->expectException(\RuntimeException::class); + + $description = new FieldDescription('foo'); + $description->setOption('bar', 'hello'); + $description->mergeOption('bar', ['exception']); + } + + public function testGetInaccessibleValue(): void + { + $quux = 'quuX'; + $foo = new Foo(); + $foo->setQuux($quux); + $ro = new \ReflectionObject($foo); + $rm = $ro->getMethod('getQuux'); + $rm->setAccessible(true); + $this->assertSame($quux, $rm->invokeArgs($foo, [])); + + $description = new FieldDescription('foo'); + + $this->expectException(NoValueException::class); + $description->getFieldValue($foo, 'quux'); + } + + public function testGetFieldValue(): void + { + $foo = new Foo(); + $foo->setBar('Bar'); + + $description = new FieldDescription('foo'); + $this->assertSame('Bar', $description->getFieldValue($foo, 'bar')); + $foo->setBar('baR'); + $this->assertSame('baR', $description->getFieldValue($foo, 'bar')); + + $foo->qux = 'Qux'; + $this->assertSame('Qux', $description->getFieldValue($foo, 'qux')); + $foo->qux = 'quX'; + $this->assertSame('quX', $description->getFieldValue($foo, 'qux')); + + $foo = new FooBoolean(); + $foo->setBar(true); + $foo->setBaz(false); + + $description = new FieldDescription('foo'); + $this->assertTrue($description->getFieldValue($foo, 'bar')); + $this->assertFalse($description->getFieldValue($foo, 'baz')); + + $this->expectException(NoValueException::class); + $description->getFieldValue($foo, 'inexistantMethod'); + } + + public function testGetFieldValueWithCodeOption(): void + { + $foo = new Foo(); + $foo->setBaz('Baz'); + + $description = new FieldDescription('foo'); + + $description->setOption('code', 'getBaz'); + $this->assertSame('Baz', $description->getFieldValue($foo, 'inexistantMethod')); + + $description->setOption('code', 'inexistantMethod'); + $this->expectException(NoValueException::class); + $description->getFieldValue($foo, 'inexistantMethod'); + } + + public function testGetFieldValueMagicCall(): void + { + $parameters = ['foo', 'bar']; + $foo = new FooCall(); + + $description = new FieldDescription('foo'); + $description->setOption('parameters', $parameters); + $this->assertSame(['inexistantMethod', $parameters], $description->getFieldValue($foo, 'inexistantMethod')); + + // repeating to cover retrieving cached getter + $this->assertSame(['inexistantMethod', $parameters], $description->getFieldValue($foo, 'inexistantMethod')); + } + + public function testGetFieldValueWithNullObject(): void + { + $foo = null; + $description = new FieldDescription('foo'); + $this->assertNull($description->getFieldValue($foo, 'bar')); + } +} diff --git a/tests/Fixtures/Entity/Foo.php b/tests/Fixtures/Entity/Foo.php new file mode 100644 index 0000000..2e07af5 --- /dev/null +++ b/tests/Fixtures/Entity/Foo.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Entity; + +class Foo +{ + public $qux; + private $bar; + + private $baz; + + private $quux; + + public function __toString() + { + return (string) $this->bar; + } + + public function getBar() + { + return $this->bar; + } + + public function setBar($bar): void + { + $this->bar = $bar; + } + + public function addBar($bar): void + { + $this->bar[] = $bar; + } + + public function getBaz() + { + return $this->baz; + } + + public function setBaz($baz): void + { + $this->baz = $baz; + } + + public function setQuux($quux): void + { + $this->quux = $quux; + } + + protected function getQuux() + { + return $this->quux; + } +} diff --git a/tests/Fixtures/Entity/FooBoolean.php b/tests/Fixtures/Entity/FooBoolean.php new file mode 100644 index 0000000..da7107e --- /dev/null +++ b/tests/Fixtures/Entity/FooBoolean.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Entity; + +class FooBoolean +{ + private $bar; + + private $baz; + + public function hasBar() + { + return $this->bar; + } + + public function setBar($bar): void + { + $this->bar = $bar; + } + + public function isBaz() + { + return $this->baz; + } + + public function setBaz($baz): void + { + $this->baz = $baz; + } +} diff --git a/tests/Fixtures/Entity/FooCall.php b/tests/Fixtures/Entity/FooCall.php new file mode 100644 index 0000000..d0ba7dc --- /dev/null +++ b/tests/Fixtures/Entity/FooCall.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Entity; + +class FooCall +{ + public function __call($method, $arguments) + { + return [$method, $arguments]; + } +} diff --git a/tests/Fixtures/Field/FieldDescription.php b/tests/Fixtures/Field/FieldDescription.php new file mode 100644 index 0000000..2134ef0 --- /dev/null +++ b/tests/Fixtures/Field/FieldDescription.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Field; + +use Sonata\DatagridBundle\Field\BaseFieldDescription; + +class FieldDescription extends BaseFieldDescription +{ + public function isIdentifier(): bool + { + throw new \BadFunctionCallException('Not implemented'); + } + + public function getValue(?object $object) + { + throw new \BadFunctionCallException('Not implemented'); + } +} diff --git a/tests/Fixtures/Mapping/AssociationMapping.php b/tests/Fixtures/Mapping/AssociationMapping.php new file mode 100644 index 0000000..0447d88 --- /dev/null +++ b/tests/Fixtures/Mapping/AssociationMapping.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Mapping; + +use Sonata\DatagridBundle\Mapping\AssociationMappingInterface; + +class AssociationMapping extends FieldMapping implements AssociationMappingInterface +{ + /** + * @var string|null + */ + protected $mappingType; + + /** + * @var string|null + */ + protected $targetModel; + + public function setMappingType(?string $mappingType): void + { + $this->mappingType = $mappingType; + } + + public function getMappingType(): ?string + { + return $this->mappingType; + } + + public function setTargetModel(?string $targetModel): void + { + $this->targetModel = $targetModel; + } + + public function getTargetModel(): ?string + { + return $this->targetModel; + } +} diff --git a/tests/Fixtures/Mapping/FieldMapping.php b/tests/Fixtures/Mapping/FieldMapping.php new file mode 100644 index 0000000..583ed73 --- /dev/null +++ b/tests/Fixtures/Mapping/FieldMapping.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\DatagridBundle\Tests\Fixtures\Mapping; + +use Sonata\DatagridBundle\Mapping\FieldMappingInterface; + +class FieldMapping implements FieldMappingInterface +{ + /** + * @var string|null + */ + protected $fieldName; + + public function setFieldName(?string $fieldName): void + { + $this->fieldName = $fieldName; + } + + public function getFieldName(): ?string + { + return $this->fieldName; + } +} diff --git a/tests/ProxyQuery/BaseProxyQueryTest.php b/tests/ProxyQuery/Doctrine/BaseProxyQueryTest.php similarity index 79% rename from tests/ProxyQuery/BaseProxyQueryTest.php rename to tests/ProxyQuery/Doctrine/BaseProxyQueryTest.php index 9dcff6f..ac1df78 100644 --- a/tests/ProxyQuery/BaseProxyQueryTest.php +++ b/tests/ProxyQuery/Doctrine/BaseProxyQueryTest.php @@ -11,12 +11,12 @@ * file that was distributed with this source code. */ -namespace Sonata\DatagridBundle\Tests\ProxyQuery; +namespace Sonata\DatagridBundle\Tests\ProxyQuery\Doctrine; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; -use Sonata\DatagridBundle\ProxyQuery\BaseProxyQuery; +use Sonata\DatagridBundle\ProxyQuery\Doctrine\ProxyQuery; /** * @author Romain Mouillard @@ -38,9 +38,7 @@ public function testFallbackOnQuerybuilder(): void ->method('getType') ->willReturn('foobar'); - $proxyQuery = $this->getMockBuilder(BaseProxyQuery::class) - ->setConstructorArgs([$queryBuilder]) - ->getMockForAbstractClass(); + $proxyQuery = new ProxyQuery($queryBuilder); $this->assertSame('foobar', $proxyQuery->getType()); }