diff --git a/Attribute/Crud.php b/Attribute/Crud.php index 065fd5c..3d547b5 100644 --- a/Attribute/Crud.php +++ b/Attribute/Crud.php @@ -10,7 +10,7 @@ final public const FETCH_MANUAL = 'manual'; /** - * @var string auto|manual + * @var 'auto'|'manual'|null * Check if fields should be fetched automatically or manually */ public ?string $fetchMode; diff --git a/Attribute/Field.php b/Attribute/Field.php index b18b1c0..6d96a1c 100644 --- a/Attribute/Field.php +++ b/Attribute/Field.php @@ -6,6 +6,9 @@ readonly class Field { + /** + * @param array $options + */ public function __construct( public ?string $label = null, public ?string $twigName = null, @@ -20,4 +23,4 @@ public function __construct( public mixed $payload = null ) {} -} \ No newline at end of file +} diff --git a/Controller/Crud.php b/Controller/Crud.php index c38ec12..2ceeef6 100644 --- a/Controller/Crud.php +++ b/Controller/Crud.php @@ -18,6 +18,7 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\QueryBuilder; +use Exception; use JetBrains\PhpStorm\ExpectedValues; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -38,15 +39,18 @@ use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use function count; /** * Creates a Crud for a given entity managed by Doctrine. - * @template T + * @template T of object */ abstract class Crud extends AbstractController { private const ITEMS_PER_PAGE = 15; protected EntityManagerInterface $em; + + /** @var ClassMetadata */ protected ClassMetadata $metadata; protected FieldService $fieldService; protected ?Request $request = null; @@ -60,10 +64,10 @@ abstract class Crud extends AbstractController /** @var EntityRepository */ protected EntityRepository $repository; - /** @var Fields|Field[] */ + /** @var Fields */ protected Fields $fields; - /** @var Filters|Filter[] */ + /** @var Filters */ protected Filters $filters; /** If the Crud is active and fully loaded */ @@ -177,6 +181,7 @@ public function getGlobalActions(): Actions } /** + * @param T $entity * Actions that can be applied to a single existing entity, such as "Edit" or "Delete" */ public function getActions($entity): Actions @@ -211,6 +216,9 @@ public function getActions($entity): Actions /** * All the actions available for a list of entities + * + * @param iterable $entities + * @return Actions[] */ public function getActionsPerEntities(iterable $entities): array { @@ -224,6 +232,8 @@ public function getActionsPerEntities(iterable $entities): array /** * The batch actions available for a lit of entities + * + * @param iterable $entities */ public function getBatchActions(iterable $entities): Actions { @@ -254,6 +264,7 @@ public function getListEntityActionsDisplayMode(): EntityActionsDisplayMode /** * Removes an entity + * @param T $entity */ public function deleteAction($entity): Response { @@ -284,7 +295,7 @@ public function deleteBatchAction(): Response return $this->redirectToList(); } $checked = $this->request->request->all('batch-actions'); - $nbChecked = \count($checked); + $nbChecked = count($checked); foreach ($checked as $k => $v) { /** @var T $entity */ $entity = $this->repository->find($k); @@ -323,13 +334,13 @@ public function toggleBooleanPostAction(Request $request, $entity): Response if (!$this->isEditableBoolean($entity)) { throw $this->createAccessDeniedException("Entity {$this->getEntity()} cannot be edited (boolean)."); } - + $index = $request->request->get('index'); $value = $request->request->getBoolean('checked'); $propertyAccessor = PropertyAccess::createPropertyAccessor(); try { $propertyAccessor->setValue($entity, $index, $value); - } catch (\Exception) { + } catch (Exception) { throw $this->createAccessDeniedException("Entity {$this->getEntity()}'s property $index cannot be read or written"); } @@ -600,7 +611,7 @@ public function exportAction(Request $request): Response foreach ($fields as $field) { try { $value = $propertyAccessor->getValue($entity, $field->getIndex()); - } catch (\Exception) { + } catch (Exception) { $value = ''; } @@ -652,6 +663,7 @@ protected function createNew(): object /** * Overrides a form type. By default, forms are created using a custom formBuilder. + * @param T $entity */ protected function overrideFormType($entity, bool $creation): ?string { @@ -703,6 +715,8 @@ public function getForm($entity, bool $creation): FormInterface /** * Returns all the entity's attributes that will be turned into Fields. + * + * @return string[] */ protected function getAllEntityFields(): array { @@ -725,9 +739,9 @@ protected function getAllEntityFields(): array } } } - $methods = $this->metadata->getReflectionClass()?->getMethods(\ReflectionMethod::IS_PUBLIC); + $methods = $this->metadata->getReflectionClass()->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { - if (\count($method->getAttributes(\Arkounay\Bundle\QuickAdminGeneratorBundle\Attribute\Field::class)) === 1) { + if (count($method->getAttributes(\Arkounay\Bundle\QuickAdminGeneratorBundle\Attribute\Field::class)) === 1) { $res[] = $method->getName(); } } @@ -759,6 +773,8 @@ protected function getFieldFetchMode(): string /** * Fields that will be used to display the list of entities. + * + * @return Fields */ protected function getListingFields(): Fields { @@ -767,6 +783,8 @@ protected function getListingFields(): Fields /** * Fields that will be used to display the detail of an entity. + * + * @return Fields */ protected function getViewFields(): Fields { @@ -775,6 +793,8 @@ protected function getViewFields(): Fields /** * Fields that will be used for the export of an entity. + * + * @return Fields */ protected function getExportFields(): Fields { @@ -783,12 +803,17 @@ protected function getExportFields(): Fields /** * Fields that will be used to automatically generate the form in the create / edit actions. + * + * @return Fields */ protected function getFormFields(): Fields { return clone $this->fields->filter(static fn(Field $field) => $field->isDisplayedInForm()); } + /** + * @return Filters + */ protected function getFilters(): Filters { return $this->filters; @@ -812,6 +837,8 @@ public function load(Request $request): void /** * Creates a Fields object without any field by default + * + * @return Fields */ protected function createFields(): Fields { @@ -820,6 +847,8 @@ protected function createFields(): Fields /** * Creates a Fields object with default fields + * + * @return Fields */ protected function createFieldsFromMetadata(): Fields { @@ -858,6 +887,9 @@ protected function createFieldsFromMetadata(): Fields return $fields; } + /** + * @return Filters + */ final protected function createFilters(): Filters { return new Filters($this->metadata, $this->fieldService); @@ -956,6 +988,7 @@ protected function createFilterForm(): FormBuilderInterface /** * All the actions that will generate Routes. * Every functions that end with "Actions" will be considered as an Action and thus, a new route will be automatically created. + * @return array */ public function getAllActions(): array { @@ -972,7 +1005,7 @@ public function getAllActions(): array /** * Finds the entity from an id. - * @return T + * @return T|null */ public function guessEntity() { @@ -992,11 +1025,12 @@ public function getDescription(): string } /** - * The default paginations options. Used to add a default sorting on the listing page. + * The default pagination options. Used to add a default sorting on the listing page. + * @param Fields $fields + * @return array */ protected function getPaginationOptions(Fields $fields): array { - /** @var Fields|Field[] $fields */ foreach ($fields as $field) { if ($field->getDefaultSortDirection() !== null) { return ['defaultSortFieldName' => 'e.' . $field->getIndex(), 'defaultSortDirection' => $field->getDefaultSortDirection()]; @@ -1017,7 +1051,7 @@ protected function getPaginationOptions(Fields $fields): array /** * True by default * If true, the responsive mode will be simplified, there won't be a table but a simple list that will display entity's toString(). - * This removes batch actions and fields informations. + * This removes batch actions and fields information. * Return false to use a responsive table with more data instead. */ protected function simpleResponsiveMode(): bool @@ -1052,6 +1086,9 @@ protected function backUrl(): string return $this->generateUrl('qag.' . $this->getRoute(), $this->getListRouteParams()); } + /** + * @return array + */ protected function getListRouteParams(): array { $params = array_merge($this->request->query->all(), $this->request->get('referer', [])); @@ -1061,11 +1098,12 @@ protected function getListRouteParams(): array /** * Checks if there are actions to display in the page, so the last column can be removed if there are not. + * @param array> $actionEntities */ protected function hasActions(array $actionEntities): bool { foreach ($actionEntities as $actions) { - if (\count($actions) > 0) { + if (count($actions) > 0) { return true; } } @@ -1085,6 +1123,7 @@ protected function hasQuickListQueryBuilderSecurity(): bool /** * Used to check if the entity is a part of the getListQueryBuilder + * @param T $entity */ protected function entityIsInList($entity): bool { @@ -1097,6 +1136,8 @@ protected function entityIsInList($entity): bool /** * Allows params overriding before twig rendering + * @param array $params + * @return array */ protected function retrieveParams(string $action, array $params): array { @@ -1107,6 +1148,9 @@ protected function retrieveParams(string $action, array $params): array ], $params); } + /** + * @return array{0: bool, 1: string|null, 2: \Symfony\Component\Form\FormInterface|null, 3: int} + */ protected function applySearchAndFiltersQueryBuilder(Request $request, QueryBuilder $queryBuilder): array { $isSearchable = $this->isSearchable(); diff --git a/Model/Action.php b/Model/Action.php index d9a2dd0..e50e6a9 100644 --- a/Model/Action.php +++ b/Model/Action.php @@ -46,14 +46,14 @@ public function setLabel(?string $label): self return $this; } + /** + * @return string[] + */ public function getClasses(): array { return $this->classes; } - /** - * @param string[] $classes - */ public function addSharedClasses(string ...$classes): self { foreach ($classes as $class) { @@ -64,9 +64,6 @@ public function addSharedClasses(string ...$classes): self return $this; } - /** - * @param string[] $classes - */ public function addClasses(string ...$classes): self { $this->classes = array_merge($this->classes, $classes); @@ -93,16 +90,25 @@ public function removeClass(string $class): self } + /** + * @return string[] + */ public function getDropdownClasses(): array { return $this->dropdownClasses; } - public function getAttributes(): ?array + /** + * @return string[] + */ + public function getAttributes(): array { return $this->attributes; } + /** + * @param string[] $attributes + */ public function setAttributes(array $attributes): self { $this->attributes = $attributes; @@ -120,6 +126,9 @@ public function addAttribute(string $key, string $content): self return $this; } + /** + * @param string[] $attributes + */ public function addAttributes(array $attributes): self { $this->attributes = array_merge($this->attributes, $attributes); @@ -131,9 +140,6 @@ public function setModal(Modal $modal): void $this->attributes = array_merge($this->attributes, $modal->toAttributes()); } - /** - * @param string[] $classes - */ public function addDropDownClasses(string ...$classes): self { $this->dropdownClasses = array_merge($this->dropdownClasses, $classes); @@ -183,4 +189,4 @@ public function setIcon(?string $icon): self return $this; } -} \ No newline at end of file +} diff --git a/Model/Actions.php b/Model/Actions.php index ac2c6bb..849c230 100644 --- a/Model/Actions.php +++ b/Model/Actions.php @@ -6,6 +6,8 @@ use function Symfony\Component\String\u; /** + * @template TKey of array-key + * @template-extends TypedArray * @method Action get(string $field) */ class Actions extends TypedArray diff --git a/Model/Field.php b/Model/Field.php index 721f83a..44f1fa9 100644 --- a/Model/Field.php +++ b/Model/Field.php @@ -11,10 +11,7 @@ class Field implements Listable { - /** - * @var string - */ - protected $index; + protected string $index; /** * @var string @@ -82,7 +79,7 @@ class Field implements Listable protected $formType; /** - * @var array + * @var array */ protected $options = []; @@ -264,11 +261,17 @@ public function setFormType(?string $formType): self return $this; } + /** + * @return array + */ public function getOptions(): array { return $this->options; } + /** + * @param array $options + */ public function setOptions(array $options): self { $this->options = $options; @@ -332,16 +335,25 @@ public function setPosition(?int $position): void $this->position = $position; } - public function getPayload() + /** + * @return mixed + */ + public function getPayload(): mixed { return $this->payload; } - public function setPayload($payload): void + /** + * @param mixed $payload + */ + public function setPayload(mixed $payload): void { $this->payload = $payload; } + /** + * @return array + */ public function guessFormOptions(): array { $options = ['label' => $this->getLabel(), 'required' => $this->isRequired()]; @@ -394,11 +406,11 @@ public function guessFormType(): ?string return match ($this->getType()) { 'decimal' => TextType::class, 'enum' => EnumType::class, - 'date' => $this->getFormType() ?? DateType::class, - 'datetime_immutable', 'datetime' => $this->getFormType() ?? DateTimeType::class, - 'relation_to_many', 'relation' => $this->getFormType() ?? EntityType::class, + 'date' => DateType::class, + 'datetime_immutable', 'datetime' => DateTimeType::class, + 'relation_to_many', 'relation' => EntityType::class, default => null, }; } -} \ No newline at end of file +} diff --git a/Model/Fields.php b/Model/Fields.php index c58a768..c9279ff 100644 --- a/Model/Fields.php +++ b/Model/Fields.php @@ -7,12 +7,16 @@ use Doctrine\ORM\Mapping\ClassMetadata; /** + * @template TEntity of object * @template-extends TypedArray * @method Field get(string $field) */ class Fields extends TypedArray { + /** + * @param ClassMetadata $metadata + */ public function __construct(private readonly ClassMetadata $metadata, private readonly FieldService $fieldService){} public function createFromIndexName(string $index): Listable @@ -25,7 +29,7 @@ protected function getType(): string return Field::class; } - public function sortByPosition(): self + public function sortByPosition(): static { uasort($this->items, static fn(Field $a, Field $b): int => $a->getPosition() <=> $b->getPosition()); @@ -33,7 +37,7 @@ public function sortByPosition(): self } - public function moveToLastPosition(string $index): self + public function moveToLastPosition(string $index): static { $maxPosition = 0; foreach ($this as $field) { @@ -45,7 +49,7 @@ public function moveToLastPosition(string $index): self return $this; } - public function moveToFirstPosition(string $index): self + public function moveToFirstPosition(string $index): static { $minPosition = 0; foreach ($this as $field) { diff --git a/Model/Filters.php b/Model/Filters.php index 058af74..01383ec 100644 --- a/Model/Filters.php +++ b/Model/Filters.php @@ -7,12 +7,16 @@ use Doctrine\ORM\Mapping\ClassMetadata; /** + * @template TEntity of object * @template-extends TypedArray * @method Filter get(string $field) */ class Filters extends TypedArray { + /** + * @param ClassMetadata $metadata + */ public function __construct(private readonly ClassMetadata $metadata, private readonly FieldService $fieldService) {} public function createFromIndexName(string $index): Listable diff --git a/Model/Form/Filter/BooleanFilter.php b/Model/Form/Filter/BooleanFilter.php index 185e49f..4fc0081 100644 --- a/Model/Form/Filter/BooleanFilter.php +++ b/Model/Form/Filter/BooleanFilter.php @@ -10,6 +10,9 @@ class BooleanFilter extends GenericFilter { + /** + * @return array + */ protected function getOptions(Filter $filter): array { return array_merge(parent::getOptions($filter), [ @@ -34,9 +37,12 @@ public function addToQueryBuilder(QueryBuilder $builder, FormInterface $form, Fi ->setParameter($index, $form->getData()[$index]); } - public function isEmpty($data): bool + /** + * @param mixed $data + */ + public function isEmpty(mixed $data): bool { return $data === ''; } -} \ No newline at end of file +} diff --git a/Model/Form/Filter/DateFilter.php b/Model/Form/Filter/DateFilter.php index ce6688c..95aaae1 100644 --- a/Model/Form/Filter/DateFilter.php +++ b/Model/Form/Filter/DateFilter.php @@ -45,7 +45,10 @@ public function addToQueryBuilder(QueryBuilder $builder, FormInterface $form, Fi }; } - public function isEmpty($data): bool + /** + * @param mixed $data + */ + public function isEmpty(mixed $data): bool { if (!isset($data['choice'])) { return false; diff --git a/Model/Form/Filter/EntityFilter.php b/Model/Form/Filter/EntityFilter.php index 097d32b..546650f 100644 --- a/Model/Form/Filter/EntityFilter.php +++ b/Model/Form/Filter/EntityFilter.php @@ -11,8 +11,14 @@ class EntityFilter extends GenericFilter { + /** + * @param array $options + */ public function __construct(private readonly string $class, protected array $options = []) {} + /** + * @return array + */ protected function getOptions(Filter $filter): array { return array_merge(parent::getOptions($filter), $this->options, ['class' => $this->class]); @@ -32,9 +38,12 @@ public function addToQueryBuilder(QueryBuilder $builder, FormInterface $form, Fi ->setParameter($index, $form->get($index)->getData()['entity']); } - public function isEmpty($data): bool + /** + * @param mixed $data + */ + public function isEmpty(mixed $data): bool { return empty($data['entity']); } -} \ No newline at end of file +} diff --git a/Model/Modal.php b/Model/Modal.php index 1e1cbe6..136157e 100644 --- a/Model/Modal.php +++ b/Model/Modal.php @@ -2,7 +2,6 @@ namespace Arkounay\Bundle\QuickAdminGeneratorBundle\Model; -use JetBrains\PhpStorm\ExpectedValues; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -156,13 +155,18 @@ public function setFocus(bool $focus): self return $this; } - #[ExpectedValues(['true', 'false', 'static'])] + /** + * @return 'true'|'false'|'static' + */ public function getBackdrop(): string { return $this->backdrop; } - public function setBackdrop(#[ExpectedValues(['true', 'false', 'static'])] string $backdrop): self + /** + * @param 'true'|'false'|'static' $backdrop + */ + public function setBackdrop(string $backdrop): self { $this->backdrop = $backdrop; return $this; @@ -205,6 +209,9 @@ protected function booleanToString(bool $bool): string { return $bool ? 'true' : 'false'; } + /** + * @return array + */ public function toAttributes(): array { $controller = $this->getController(); @@ -227,4 +234,4 @@ public function toAttributes(): array ]; } -} \ No newline at end of file +} diff --git a/Model/TypedArray.php b/Model/TypedArray.php index 8b1bde2..af52712 100644 --- a/Model/TypedArray.php +++ b/Model/TypedArray.php @@ -6,7 +6,7 @@ /** * @internal * @template TKey of array-key - * @template T + * @template T of Listable * @template-implements \IteratorAggregate * @template-implements \ArrayAccess */ @@ -14,7 +14,7 @@ abstract class TypedArray implements \IteratorAggregate, \ArrayAccess, \Countabl { /** - * @var Listable[] + * @var array */ protected array $items = []; @@ -26,7 +26,7 @@ abstract public function createFromIndexName(string $index): Listable; abstract protected function getType(): string; /** - * @return T + * @phpstan-return T */ public function get(string $field): Listable { @@ -34,9 +34,9 @@ public function get(string $field): Listable } /** - * @param T|string $field + * @phpstan-param T|string $field */ - public function add(Listable|string $field): self + public function add(Listable|string $field): static { $type = $this->getType(); if ($field instanceof $type) { @@ -55,20 +55,23 @@ public function add(Listable|string $field): self return $this; } - public function remove(string $fieldIndex): self + public function remove(string $fieldIndex): static { unset($this->items[$fieldIndex]); return $this; } + /** + * @return \ArrayIterator + */ public function getIterator(): \Traversable { return new \ArrayIterator($this->items); } /** - * @param Listable $value + * @param T $value */ public function offsetSet(mixed $offset, $value): void { @@ -81,17 +84,24 @@ public function offsetSet(mixed $offset, $value): void } } + /** + * @param TKey $offset + */ public function offsetExists($offset): bool { return isset($this->items[$offset]); } + /** + * @param TKey $offset + */ public function offsetUnset($offset): void { unset($this->items[$offset]); } /** + * @param TKey $offset * @return T */ public function offsetGet($offset): Listable @@ -104,13 +114,16 @@ public function isEmpty(): bool return empty($this->items); } - public function clear(): self + public function clear(): static { $this->items = []; return $this; } + /** + * @param iterable|iterable $fields + */ public function set(iterable $fields): void { $this->clear(); @@ -124,7 +137,7 @@ public function count(): int return \count($this->items); } - public function moveToLastPosition(string $index): self + public function moveToLastPosition(string $index): static { $tmp = $this->items[$index]; unset($this->items[$index]); @@ -133,7 +146,7 @@ public function moveToLastPosition(string $index): self return $this; } - public function moveToFirstPosition(string $index): self + public function moveToFirstPosition(string $index): static { $this->items = array_merge([$index => $this->items[$index]], $this->items); @@ -142,12 +155,16 @@ public function moveToFirstPosition(string $index): self public function contains(string $index): bool { - return isset($this->items); + return isset($this->items[$index]); } - public function filter(callable $callback): self + /** + * @param callable(T, TKey): bool $callback + * @return static + */ + public function filter(callable $callback): static { - $this->items = array_filter($this->items, $callback); + $this->items = array_filter($this->items, $callback, ARRAY_FILTER_USE_BOTH); return $this; }