diff --git a/composer.json b/composer.json index 190784be8..b86579be5 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,6 @@ ], "require": { "php": "^8.2", - "cuyz/valinor": "^1.0.0", "doctrine/annotations": "^2.0", "friendsofsymfony/ckeditor-bundle": "^2.4", "johnkrovitch/configuration": "^2.2", diff --git a/config/services/resources.yaml b/config/services/resources.yaml index 291861b25..1ede0bc62 100644 --- a/config/services/resources.yaml +++ b/config/services/resources.yaml @@ -1,20 +1,30 @@ services: - lag_admin.resource.registry: '@LAG\AdminBundle\Resource\Registry\ResourceRegistryInterface' - LAG\AdminBundle\Resource\Registry\ResourceRegistryInterface: - class: LAG\AdminBundle\Resource\Registry\ResourceRegistry + lag_admin.resource.registry: '@LAG\AdminBundle\Metadata\Registry\ResourceRegistryInterface' + LAG\AdminBundle\Metadata\Registry\ResourceRegistryInterface: + class: LAG\AdminBundle\Metadata\Registry\ResourceRegistry arguments: $resourcePaths: '%lag_admin.resource_paths%' $locator: '@lag_admin.metadata.locator' $resourceFactory: '@lag_admin.resource.factory' + LAG\AdminBundle\Metadata\Registry\CacheRegistryDecorator: + decorates: 'lag_admin.resource.registry' + arguments: + $decorated: '@.inner' + lag_admin.metadata.locator: '@LAG\AdminBundle\Metadata\Locator\MetadataLocatorInterface' LAG\AdminBundle\Metadata\Locator\MetadataLocatorInterface: class: LAG\AdminBundle\Metadata\Locator\CompositeLocator arguments: $locators: !tagged_iterator lag_admin.resource.locator + $kernel: '@kernel' LAG\AdminBundle\Metadata\Locator\AttributeLocator: tags: ['lag_admin.resource.locator'] - LAG\AdminBundle\Metadata\Locator\YamlLocator: - tags: ['lag_admin.resource.locator'] + lag_admin.resource.context: '@LAG\AdminBundle\Metadata\Context\ResourceContextInterface' + LAG\AdminBundle\Metadata\Context\ResourceContextInterface: + class: LAG\AdminBundle\Metadata\Context\ResourceContext + arguments: + $parametersExtractor: '@lag_admin.request.parameters_extractor' + $resourceRegistry: '@lag_admin.resource.registry' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 77a6a87b8..64e9a892b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,42 +1,44 @@ - - - - src - - - - - - - - - tests/phpunit - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + tests/phpunit + + + + + + + + + + + + + + + + + + + + + + + + + + + src + + diff --git a/src/Event/OperationEvents.php b/src/Event/OperationEvents.php index 1c46aac0f..94bf180d3 100644 --- a/src/Event/OperationEvents.php +++ b/src/Event/OperationEvents.php @@ -8,8 +8,10 @@ enum OperationEvents: string { public const OPERATION_CREATE = 'lag_admin.operation.create'; public const OPERATION_CREATED = 'lag_admin.operation.created'; - public const OPERATION_CREATE_RESOURCE_PATTERN = 'lag_admin.%s.operation.%s.create'; - public const OPERATION_CREATED_RESOURCE_PATTERN = 'lag_admin.%s.operation.%s.created'; + + public const RESOURCE_OPERATION_CREATE_PATTERN = 'lag_admin.%s.operation.%s.create'; + public const RESOURCE_OPERATION_CREATED_PATTERN = 'lag_admin.%s.operation.%s.created'; + public const OPERATION_CREATE_PATTERN = 'lag_admin.%s.operation.create'; public const OPERATION_CREATED_PATTERN = 'lag_admin.%s.operation.created'; } diff --git a/src/Metadata/AdminResource.php b/src/Metadata/AdminResource.php index f27a7e067..bc3f8fddb 100644 --- a/src/Metadata/AdminResource.php +++ b/src/Metadata/AdminResource.php @@ -8,34 +8,71 @@ use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProcessor; use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProvider; use LAG\AdminBundle\Exception\Operation\OperationMissingException; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class AdminResource { - private OperationInterface $currentOperation; - public function __construct( + #[NotBlank] private ?string $name = null, + + #[NotBlank] private ?string $dataClass = null, + + #[NotBlank(allowNull: true)] private ?string $title = null, + + #[NotBlank(allowNull: true)] private ?string $group = null, + + #[NotBlank(allowNull: true)] private ?string $icon = null, + /** @var OperationInterface[] $operations */ + #[Length(min: 1)] private array $operations = [ - new Index(), + new GetCollection(), + new Get(), new Create(), new Update(), new Delete(), - new Show(), ], - private string $processor = ORMDataProcessor::class, + + #[NotBlank] + private ?string $processor = ORMDataProcessor::class, + + #[NotBlank] private string $provider = ORMDataProvider::class, + /** @var string[] $identifiers */ private array $identifiers = ['id'], - private string $routePattern = 'lag_admin.{resource}.{operation}', + + private string $routePattern = '{application}.{resource}.{operation}', + private ?string $routePrefix = '/{resourceName}', - private ?string $translationPattern = null, + + private ?string $translationPattern = '{application}.{resource}.{message}', + private ?string $translationDomain = null, + + #[NotBlank] + private ?string $applicationName = null, + + private ?string $formType = null, + + private array $formOptions = [], + + private bool $validation = true, + + private ?array $validationContext = null, + + private bool $ajax = true, + + private ?array $normalizationContext = null, + + private ?array $denormalizationContext = null, ) { } @@ -181,6 +218,19 @@ public function withRoutePattern(string $routePattern): self return $self; } + public function getRoutePrefix(): ?string + { + return $this->routePrefix; + } + + public function withRoutePrefix(?string $prefix): self + { + $self = clone $this; + $self->routePrefix = $prefix; + + return $self; + } + public function getIdentifiers(): array { return $this->identifiers; @@ -194,54 +244,132 @@ public function withIdentifiers(array $identifiers): self return $self; } - public function getCurrentOperation(): OperationInterface + public function getTranslationPattern(): ?string { - return $this->currentOperation; + return $this->translationPattern; } - public function withCurrentOperation(OperationInterface $currentOperation): self + public function withTranslationPattern(?string $translationPattern): self { $self = clone $this; - $self->currentOperation = $currentOperation; + $self->translationPattern = $translationPattern; return $self; } - public function getRoutePrefix(): ?string + public function getTranslationDomain(): ?string { - return $this->routePrefix; + return $this->translationDomain; } - public function withRoutePrefix(?string $prefix): self + public function withTranslationDomain(?string $translationDomain): self { $self = clone $this; - $self->routePrefix = $prefix; + $self->translationDomain = $translationDomain; return $self; } - public function getTranslationPattern(): ?string + public function getApplicationName(): ?string { - return $this->translationPattern; + return $this->applicationName; } - public function withTranslationPattern(?string $translationPattern): self + public function withApplicationName(?string $applicationName): self { $self = clone $this; - $self->translationPattern = $translationPattern; + $self->applicationName = $applicationName; return $self; } - public function getTranslationDomain(): ?string + public function getFormType(): ?string { - return $this->translationDomain; + return $this->formType; } - public function withTranslationDomain(?string $translationDomain): self + public function withFormType(?string $formType): self { $self = clone $this; - $self->translationDomain = $translationDomain; + $self->formType = $formType; + + return $self; + } + + public function getFormOptions(): ?array + { + return $this->formOptions; + } + + public function withFormOptions(?array $formOptions): self + { + $self = clone $this; + $self->formOptions = $formOptions; + + return $self; + } + + public function isValidationEnabled(): bool + { + return $this->validation; + } + + public function withValidation(bool $validation): self + { + $self = clone $this; + $self->validation = $validation; + + return $self; + } + + public function getValidationContext(): ?array + { + return $this->validationContext; + } + + public function withValidationContext(array $context): self + { + $self = clone $this; + $self->validationContext = $context; + + return $self; + } + + public function hasAjax(): bool + { + return $this->ajax; + } + + public function withAjax(bool $ajax): self + { + $self = clone $this; + $self->ajax = $ajax; + + return $self; + } + + public function getNormalizationContext(): ?array + { + return $this->normalizationContext; + } + + public function withNormalizationContext(array $context): self + { + $self = clone $this; + $self->normalizationContext = $context; + + return $self; + } + + public function getDenormalizationContext(): ?array + { + return $this->denormalizationContext; + } + + public function withDenormalizationContext(array $context): self + { + $self = clone $this; + $self->denormalizationContext = $context; return $self; } diff --git a/src/Metadata/AttributesHelper.php b/src/Metadata/AttributesHelper.php index 3d5d950cd..0f2616516 100644 --- a/src/Metadata/AttributesHelper.php +++ b/src/Metadata/AttributesHelper.php @@ -4,6 +4,8 @@ namespace LAG\AdminBundle\Metadata; +use LAG\AdminBundle\Entity\Mapping\Sluggable; + class AttributesHelper { public static function getReflectionClassesFromDirectories(string $path): \Iterator @@ -44,4 +46,16 @@ public static function getReflectionClassesFromDirectories(string $path): \Itera } } } + + public static function getAttributes(string $sourceClass, string $attributeClass): array + { + $reflectionClass = new \ReflectionClass($sourceClass); + $attributes = []; + + foreach ($reflectionClass->getAttributes($attributeClass) as $attribute) { + $attributes[] = $attribute->newInstance(); + } + + return $attributes; + } } diff --git a/src/Metadata/CollectionOperation.php b/src/Metadata/CollectionOperation.php index f92ead20c..a8f3945f5 100644 --- a/src/Metadata/CollectionOperation.php +++ b/src/Metadata/CollectionOperation.php @@ -6,7 +6,10 @@ use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProcessor; use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProvider; +use LAG\AdminBundle\Form\Type\Resource\FilterType; use LAG\AdminBundle\Metadata\Filter\FilterInterface; +use LAG\AdminBundle\Validation\Constraint\GridExist; +use LAG\AdminBundle\Validation\Constraint\TemplateValid; abstract class CollectionOperation extends Operation implements CollectionOperationInterface { @@ -22,46 +25,53 @@ public function __construct( array $routeParameters = [], array $methods = [], string $path = null, - ?string $targetRoute = null, - array $targetRouteParameters = [], + ?string $redirectRoute = null, + array $redirectRouteParameters = [], array $properties = [], ?string $formType = null, array $formOptions = [], - string $processor = ORMDataProcessor::class, + ?string $processor = ORMDataProcessor::class, string $provider = ORMDataProvider::class, array $identifiers = ['id'], ?array $contextualActions = null, ?array $itemActions = null, + ?string $redirectResource = null, + ?string $redirectOperation = null, private bool $pagination = true, private int $itemPerPage = 25, private string $pageParameter = 'page', private array $criteria = [], private array $orderBy = [], private ?array $filters = null, - private ?string $gridTemplate = '@LAGAdmin/grid/table_grid.html.twig', + #[GridExist] + private ?string $grid = 'table', + private ?string $filterFormType = FilterType::class, + private array $filterFormOptions = [], ) { parent::__construct( - $name, - $title, - $description, - $icon, - $template, - $permissions, - $controller, - $route, - $routeParameters, - $methods, - $path, - $targetRoute, - $targetRouteParameters, - $properties, - $formType, - $formOptions, - $processor, - $provider, - $identifiers, - $contextualActions, - $itemActions, + name: $name, + title: $title, + description: $description, + icon: $icon, + template: $template, + permissions: $permissions, + controller: $controller, + route: $route, + routeParameters: $routeParameters, + methods: $methods, + path: $path, + redirectRoute: $redirectRoute, + redirectRouteParameters: $redirectRouteParameters, + properties: $properties, + formType: $formType, + formOptions: $formOptions, + processor: $processor, + provider: $provider, + identifiers: $identifiers, + contextualActions: $contextualActions, + itemActions: $itemActions, + redirectResource: $redirectResource, + redirectOperation: $redirectOperation, ); } @@ -159,15 +169,41 @@ public function withFilters(array $filters): self return $self; } - public function getGridTemplate(): ?string + public function getGrid(): ?string { - return $this->gridTemplate; + return $this->grid; } public function withGridTemplate(?string $gridTemplate): self { $self = clone $this; - $self->gridTemplate = $gridTemplate; + $self->grid = $gridTemplate; + + return $self; + } + + public function getFilterFormType(): ?string + { + return $this->filterFormType; + } + + public function withFilterFormType(?string $filterForm): self + { + $self = clone $this; + $self->filterFormType = $filterForm; + + return $self; + } + + public function getFilterFormOptions(): array + { + return $this->filterFormOptions; + } + + public function withFilterFormOptions(array $filterFormOptions): self + { + $self = clone $this; + $self->filterFormOptions = $filterFormOptions; return $self; } diff --git a/src/Metadata/CollectionOperationInterface.php b/src/Metadata/CollectionOperationInterface.php index 4d0464fd3..9ae1887db 100644 --- a/src/Metadata/CollectionOperationInterface.php +++ b/src/Metadata/CollectionOperationInterface.php @@ -6,6 +6,10 @@ use LAG\AdminBundle\Metadata\Filter\FilterInterface; +/** + * Interface for collection operations. It adds the required attributes for collection handling to the item operation + * interface + */ interface CollectionOperationInterface extends OperationInterface { public function hasPagination(): bool; @@ -28,16 +32,25 @@ public function getOrderBy(): array; public function withOrderBy(array $orderBy): self; - /** @return FilterInterface[]|null */ + /** @return array|null */ public function getFilters(): ?array; public function getFilter(string $name): ?FilterInterface; public function hasFilters(): bool; + /** @param array $filters */ public function withFilters(array $filters): self; - public function getGridTemplate(): ?string; + public function getGrid(): ?string; public function withGridTemplate(?string $gridTemplate): self; + + public function getFilterFormType(): ?string; + + public function withFilterFormType(?string $filterForm): self; + + public function getFilterFormOptions(): array; + + public function withFilterFormOptions(array $filterFormOptions): self; } diff --git a/src/Metadata/Context/ResourceContext.php b/src/Metadata/Context/ResourceContext.php new file mode 100644 index 000000000..1d45e2f7a --- /dev/null +++ b/src/Metadata/Context/ResourceContext.php @@ -0,0 +1,41 @@ +parametersExtractor->supports($request)) { + throw new Exception('The current request is not supported by any admin resource'); + } + $resourceName = $this->parametersExtractor->getResourceName($request); + $operationName = $this->parametersExtractor->getOperationName($request); + $resource = $this->resourceRegistry->get($resourceName); + + return $resource->getOperation($operationName); + } + + public function getResource(Request $request): AdminResource + { + return $this->getOperation($request)->getResource(); + } + + public function supports(Request $request): bool + { + return $this->parametersExtractor->supports($request); + } +} diff --git a/src/Metadata/Context/ResourceContextInterface.php b/src/Metadata/Context/ResourceContextInterface.php new file mode 100644 index 000000000..876bb2cf9 --- /dev/null +++ b/src/Metadata/Context/ResourceContextInterface.php @@ -0,0 +1,16 @@ +getName(), )); $this->eventDispatcher->dispatch($event, sprintf( - OperationEvents::OPERATION_CREATE_RESOURCE_PATTERN, + OperationEvents::RESOURCE_OPERATION_CREATE_PATTERN, $resource->getName(), $operationDefinition->getName(), )); @@ -70,7 +70,7 @@ private function dispatchPostEvents(AdminResource $resource, OperationInterface $resource->getName(), )); $this->eventDispatcher->dispatch($event, sprintf( - OperationEvents::OPERATION_CREATED_RESOURCE_PATTERN, + OperationEvents::RESOURCE_OPERATION_CREATED_PATTERN, $resource->getName(), $operation->getName(), )); diff --git a/src/Metadata/Factory/PropertyFactory.php b/src/Metadata/Factory/PropertyFactory.php index 1e0cdc7dd..93be5a354 100644 --- a/src/Metadata/Factory/PropertyFactory.php +++ b/src/Metadata/Factory/PropertyFactory.php @@ -6,6 +6,7 @@ use LAG\AdminBundle\Exception\Validation\InvalidPropertyCollectionException; use LAG\AdminBundle\Metadata\OperationInterface; +use LAG\AdminBundle\Metadata\Property\Link; use LAG\AdminBundle\Metadata\Property\PropertyInterface; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -35,7 +36,11 @@ public function createCollection(OperationInterface $operation): array } if (\count($operationErrors) > 0) { - throw new InvalidPropertyCollectionException($operationErrors, $operation->getResource()->getName(), $operation->getName()); + throw new InvalidPropertyCollectionException( + $operationErrors, + $operation->getResource()->getName(), + $operation->getName()) + ; } return $properties; @@ -48,17 +53,15 @@ private function initializeProperty(OperationInterface $operation, PropertyInter } if (!$property->getLabel()) { - $label = null; - if ($operation->getResource()->getTranslationPattern()) { $label = u($operation->getResource()->getTranslationPattern()) + ->replace('{application}', $operation->getResource()->getApplicationName()) ->replace('{resource}', $operation->getResource()->getName()) - ->replace('{property}', u($property->getName())->snake()->toString()) + ->replace('{message}', u($property->getName())->snake()->toString()) ->lower() ->toString() ; - } - if (!$label) { + } else { $label = u($property->getName()) ->replace('_', ' ') ->title() diff --git a/src/Metadata/Factory/ResourceFactoryCacheDecorator.php b/src/Metadata/Factory/ResourceFactoryCacheDecorator.php deleted file mode 100644 index ac6c6c2e8..000000000 --- a/src/Metadata/Factory/ResourceFactoryCacheDecorator.php +++ /dev/null @@ -1,28 +0,0 @@ -getName(); - - if (!\array_key_exists($resourceName, $this->cache)) { - $this->cache[$resourceName] = $this->decorated->create($definition); - } - - return $this->cache[$resourceName]; - } -} diff --git a/src/Metadata/Filter/Filter.php b/src/Metadata/Filter/Filter.php index 670a75d12..f5eab0bb6 100644 --- a/src/Metadata/Filter/Filter.php +++ b/src/Metadata/Filter/Filter.php @@ -5,16 +5,22 @@ namespace LAG\AdminBundle\Metadata\Filter; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Validator\Constraints as Assert; #[\Attribute] class Filter implements FilterInterface { public function __construct( + #[Assert\NotBlank] private string $name, + #[Assert\NotBlank] private ?string $propertyPath = null, + #[Assert\NotBlank] private string $comparator = '=', + #[Assert\NotBlank] private string $operator = 'and', private mixed $data = null, + #[Assert\NotBlank] private string $formType = TextType::class, private array $formOptions = [], ) { diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php new file mode 100644 index 000000000..b913bce92 --- /dev/null +++ b/src/Metadata/Get.php @@ -0,0 +1,67 @@ +before('/')->after('@')->toString(); + $resourceDirectory = u($this->kernel->getBundle($bundleName)->getPath()) + ->ensureEnd('/') + ->append(u($resourceDirectory)->after('/')->toString()) + ->toString() + ; + } + /** @var MetadataLocatorInterface $locator */ foreach ($this->locators as $locator) { foreach ($locator->locateCollection($resourceDirectory) as $resource) { diff --git a/src/Metadata/Locator/YamlLocator.php b/src/Metadata/Locator/YamlLocator.php deleted file mode 100644 index 316d80159..000000000 --- a/src/Metadata/Locator/YamlLocator.php +++ /dev/null @@ -1,47 +0,0 @@ -exists($resourceDirectory) || !is_dir($resourceDirectory)) { - throw new Exception(sprintf('The resources path %s does not exists or is not a directory', $resourceDirectory)); - } - $finder = new Finder(); - $finder - ->files() - ->name('*.yaml') - ->in($resourceDirectory) - ; - $resources = []; - - foreach ($finder as $fileInfo) { - $yaml = Yaml::parse(file_get_contents($fileInfo->getRealPath()), Yaml::PARSE_CUSTOM_TAGS); - - foreach ($yaml as $name => $configuration) { - $resource = (new MapperBuilder()) - ->mapper() - ->map(AdminResource::class, Source::array($configuration ?? [])) - ; - $resource = $resource->withName($name); - $resources[] = $resource; - } - } - - return $resources; - } -} diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index 9210f7d58..0198e7783 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -6,6 +6,7 @@ use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProcessor; use LAG\AdminBundle\Bridge\Doctrine\ORM\State\ORMDataProvider; +use LAG\AdminBundle\Metadata\Property\PropertyInterface; use Symfony\Component\Validator\Constraints as Assert; abstract class Operation implements OperationInterface @@ -15,31 +16,67 @@ abstract class Operation implements OperationInterface public function __construct( #[Assert\NotBlank(message: 'The operation name should not be empty')] private ?string $name = null, + #[Assert\Length(max: 255, maxMessage: 'The operation title should be shorter than 255 characters')] private ?string $title = null, + private ?string $description = null, + #[Assert\Length(max: 255, maxMessage: 'The operation icon should be shorter than 255 characters')] private ?string $icon = null, + #[Assert\NotBlank(message: 'The operation template should not be empty')] private ?string $template = null, + private ?array $permissions = [], + #[Assert\NotBlank(message: 'The operation controller should not be empty')] private ?string $controller = null, + #[Assert\NotBlank(message: 'The operation has an empty route')] private ?string $route = null, + private ?array $routeParameters = null, + private array $methods = [], + private ?string $path = null, - private ?string $targetRoute = null, - private ?array $targetRouteParameters = null, + + private ?string $redirectRoute = null, + + #[Assert\NotNull] + private ?array $redirectRouteParameters = null, + + /** @var PropertyInterface[] */ private array $properties = [], + private ?string $formType = null, + private array $formOptions = [], + private string $processor = ORMDataProcessor::class, + private string $provider = ORMDataProvider::class, + private array $identifiers = ['id'], + private ?array $contextualActions = null, + private ?array $itemActions = null, + + private ?string $redirectResource = null, + + private ?string $redirectOperation = null, + + private ?bool $validation = true, + + private ?array $validationContext = null, + + private bool $ajax = true, + + private ?array $normalizationContext = null, + + private ?array $denormalizationContext = null, ) { } @@ -173,28 +210,28 @@ public function withPath(?string $path): self return $self; } - public function getTargetRoute(): ?string + public function getRedirectRoute(): ?string { - return $this->targetRoute; + return $this->redirectRoute; } - public function withTargetRoute(?string $targetRoute): self + public function withRedirectRoute(?string $targetRoute): self { $self = clone $this; - $self->targetRoute = $targetRoute; + $self->redirectRoute = $targetRoute; return $self; } - public function getTargetRouteParameters(): ?array + public function getRedirectRouteParameters(): ?array { - return $this->targetRouteParameters; + return $this->redirectRouteParameters; } - public function withTargetRouteParameters(?array $targetRouteParameters): self + public function withRedirectRouteParameters(?array $targetRouteParameters): self { $self = clone $this; - $self->targetRouteParameters = $targetRouteParameters; + $self->redirectRouteParameters = $targetRouteParameters; return $self; } @@ -212,6 +249,21 @@ public function withProperties(array $properties): self return $self; } + public function withProperty(PropertyInterface $newProperty): OperationInterface + { + $self = clone $this; + $found = false; + + foreach ($self->properties as $index => $property) { + if ($property->getName() === $newProperty->getName()) { + $self->properties[$index] = $newProperty; + $found = true; + } + } + + return $self; + } + public function getFormType(): ?string { return $this->formType; @@ -328,4 +380,95 @@ public function withItemActions(array $itemActions): self return $self; } + + public function getRedirectResource(): ?string + { + return $this->redirectResource; + } + + public function withRedirectResource(?string $redirectResource): self + { + $self = clone $this; + $self->redirectResource = $redirectResource; + + return $self; + } + + public function getRedirectOperation(): ?string + { + return $this->redirectOperation; + } + + public function withRedirectOperation(?string $redirectOperation): self + { + $self = clone $this; + $self->redirectOperation = $redirectOperation; + + return $self; + } + + public function isValidationEnabled(): ?bool + { + return $this->validation; + } + + public function withValidation(bool $validation): self + { + $self = clone $this; + $self->validation = $validation; + + return $self; + } + + public function getValidationContext(): ?array + { + return $this->validationContext; + } + + public function withValidationContext(array $context): self + { + $self = clone $this; + $self->validationContext = $context; + + return $self; + } + + public function useAjax(): bool + { + return $this->ajax; + } + + public function withAjax(bool $ajax): self + { + $self = clone $this; + $self->ajax = $ajax; + + return $self; + } + + public function getNormalizationContext(): ?array + { + return $this->normalizationContext; + } + + public function withNormalizationContext(array $context): self + { + $self = clone $this; + $self->normalizationContext = $context; + + return $self; + } + + public function getDenormalizationContext(): ?array + { + return $this->denormalizationContext; + } + + public function withDenormalizationContext(array $context): self + { + $self = clone $this; + $self->denormalizationContext = $context; + + return $self; + } } diff --git a/src/Metadata/OperationInterface.php b/src/Metadata/OperationInterface.php index ad1b408a8..a9df167e0 100644 --- a/src/Metadata/OperationInterface.php +++ b/src/Metadata/OperationInterface.php @@ -48,13 +48,13 @@ public function getPath(): ?string; public function withPath(?string $path): self; - public function getTargetRoute(): ?string; + public function getRedirectRoute(): ?string; - public function withTargetRoute(?string $targetRoute): self; + public function withRedirectRoute(?string $targetRoute): self; - public function getTargetRouteParameters(): ?array; + public function getRedirectRouteParameters(): ?array; - public function withTargetRouteParameters(?array $targetRouteParameters): self; + public function withRedirectRouteParameters(?array $targetRouteParameters): self; /** * @return PropertyInterface[] @@ -63,6 +63,8 @@ public function getProperties(): array; public function withProperties(array $properties): self; + public function withProperty(PropertyInterface $newProperty): self; + public function getFormType(): ?string; public function withFormType(?string $formType): self; @@ -102,4 +104,32 @@ public function getItemActions(): ?array; /** @param array $itemActions Link[]|null */ public function withItemActions(array $itemActions): self; + + public function getRedirectResource(): ?string; + + public function withRedirectResource(?string $redirectResource): self; + + public function getRedirectOperation(): ?string; + + public function withRedirectOperation(?string $redirectOperation): self; + + public function isValidationEnabled(): ?bool; + + public function withValidation(bool $validation): self; + + public function getValidationContext(): ?array; + + public function withValidationContext(array $context): self; + + public function useAjax(): bool; + + public function withAjax(bool $ajax): self; + + public function getNormalizationContext(): ?array; + + public function withNormalizationContext(array $context): self; + + public function getDenormalizationContext(): ?array; + + public function withDenormalizationContext(array $context): self; } diff --git a/src/Metadata/Property/AbstractProperty.php b/src/Metadata/Property/AbstractProperty.php index 0ad88f445..6b68624b7 100644 --- a/src/Metadata/Property/AbstractProperty.php +++ b/src/Metadata/Property/AbstractProperty.php @@ -4,27 +4,25 @@ namespace LAG\AdminBundle\Metadata\Property; -use LAG\AdminBundle\Grid\DataTransformer\DataTransformerInterface; -use Symfony\Component\Validator\Constraints\Length; -use Symfony\Component\Validator\Constraints\NotBlank; +use LAG\AdminBundle\Validation\Constraint\TemplateValid; +use Symfony\Component\Validator\Constraints as Assert; abstract class AbstractProperty implements PropertyInterface { public function __construct( - #[NotBlank] - #[Length(min: 1, max: 255)] + #[Assert\NotBlank] + #[Assert\Length(min: 1, max: 255)] private string $name, - #[NotBlank] + #[Assert\NotBlank] private ?string $propertyPath, private ?string $label = null, + #[TemplateValid] private ?string $template = null, - private bool $mapped = true, private bool $sortable = true, - private bool $translation = false, + private bool $translatable = false, private ?string $translationDomain = null, private array $attr = [], private array $headerAttr = [], - private ?DataTransformerInterface $dataTransformer = null, ) { } @@ -80,19 +78,6 @@ public function withTemplate(?string $template): self return $self; } - public function isMapped(): bool - { - return $this->mapped; - } - - public function withMapped(bool $mapped): self - { - $self = clone $this; - $self->mapped = $mapped; - - return $self; - } - public function isSortable(): bool { return $this->sortable; @@ -106,28 +91,15 @@ public function withSortable(bool $sortable): self return $self; } - public function hasTranslation(): bool - { - return $this->translation; - } - - public function withTranslation(bool $translation): self - { - $self = clone $this; - $self->translation = $translation; - - return $self; - } - - public function getTranslationDomain(): ?string + public function isTranslatable(): bool { - return $this->translationDomain; + return $this->translatable; } - public function withTranslationDomain(?string $translationDomain): self + public function withTranslatable(bool $translatable): self { $self = clone $this; - $self->translationDomain = $translationDomain; + $self->translatable = $translatable; return $self; } @@ -158,15 +130,15 @@ public function withHeaderAttr(array $headerAttr): self return $self; } - public function getDataTransformer(): ?DataTransformerInterface + public function getTranslationDomain(): ?string { - return $this->dataTransformer; + return $this->translationDomain; } - public function withDataTransformer(?DataTransformerInterface $dataTransformer): self + public function withTranslationDomain(?string $translationDomain): self { $self = clone $this; - $self->dataTransformer = $dataTransformer; + $self->translationDomain = $translationDomain; return $self; } diff --git a/src/Metadata/Property/Boolean.php b/src/Metadata/Property/Boolean.php new file mode 100644 index 000000000..345e024d0 --- /dev/null +++ b/src/Metadata/Property/Boolean.php @@ -0,0 +1,32 @@ +localization; - } - - public function withLocalization(bool $localization): self - { - $self = clone $this; - $self->localization = $localization; - - return $self; - } } diff --git a/src/Metadata/Property/Image.php b/src/Metadata/Property/Image.php new file mode 100644 index 000000000..d942c81c7 --- /dev/null +++ b/src/Metadata/Property/Image.php @@ -0,0 +1,30 @@ +routeParameters as $name => $value) { + if ($value !== null || !$accessor->isReadable(objectOrArray: $data, propertyPath: $name)) { + continue; + } + $this->routeParameters[$name] = $accessor->getValue(objectOrArray: $data, propertyPath: $name); + } + } + + public function getRoute(): ?string + { + return $this->route; + } + + public function withRoute(?string $routeName): self + { + $self = clone $this; + $self->route = $routeName; + + return $self; + } + + public function getRouteParameters(): ?array + { + return $this->routeParameters; + } + + public function setRouteParameters(?array $routeParameters): self + { + $self = clone $this; + $self->routeParameters = $routeParameters; + + return $self; + } +} diff --git a/src/Metadata/Property/LinkProperty.php b/src/Metadata/Property/LinkProperty.php deleted file mode 100644 index 82c5c422e..000000000 --- a/src/Metadata/Property/LinkProperty.php +++ /dev/null @@ -1,62 +0,0 @@ -routeName; - } - - public function withRouteName(?string $routeName): self - { - $self = clone $this; - $self->routeName = $routeName; - - return $self; - } - - public function getRouteParameters(): ?array - { - return $this->routeParameters; - } - - public function setRouteParameters(?array $routeParameters): self - { - $self = clone $this; - $self->routeParameters = $routeParameters; - - return $self; - } -} diff --git a/src/Metadata/Property/CountProperty.php b/src/Metadata/Property/Mapped.php similarity index 60% rename from src/Metadata/Property/CountProperty.php rename to src/Metadata/Property/Mapped.php index 82474ff0e..f93452d28 100644 --- a/src/Metadata/Property/CountProperty.php +++ b/src/Metadata/Property/Mapped.php @@ -4,36 +4,35 @@ namespace LAG\AdminBundle\Metadata\Property; -use LAG\AdminBundle\Grid\DataTransformer\CallbackTransformer; - -class CountProperty extends StringProperty +class Mapped extends AbstractProperty { public function __construct( string $name, ?string $propertyPath = null, ?string $label = null, - ?string $template = '@LAGAdmin/grid/properties/count.html.twig', - bool $mapped = true, + ?string $template = '@LAGAdmin/grids/properties/mapped.html.twig', bool $sortable = true, - bool $translation = false, + bool $translatable = true, ?string $translationDomain = null, array $attr = [], array $headerAttr = [], + private array $map = [], ) { parent::__construct( name: $name, propertyPath: $propertyPath, label: $label, template: $template, - mapped: $mapped, sortable: $sortable, - translation: $translation, + translatable: $translatable, translationDomain: $translationDomain, attr: $attr, headerAttr: $headerAttr, - dataTransformer: new CallbackTransformer(function ($data) { - return \count($data); - }), ); } + + public function getMap(): array + { + return $this->map; + } } diff --git a/src/Metadata/Property/MappedProperty.php b/src/Metadata/Property/MappedProperty.php deleted file mode 100644 index 78846b1c1..000000000 --- a/src/Metadata/Property/MappedProperty.php +++ /dev/null @@ -1,45 +0,0 @@ -map[$data] ?? null; - }) - ); - } - - public function getMap(): array - { - return $this->map; - } -} diff --git a/src/Metadata/Property/PropertyInterface.php b/src/Metadata/Property/PropertyInterface.php index 9ada67e35..94f38d0c7 100644 --- a/src/Metadata/Property/PropertyInterface.php +++ b/src/Metadata/Property/PropertyInterface.php @@ -24,21 +24,13 @@ public function getTemplate(): ?string; public function withTemplate(?string $template): self; - public function isMapped(): bool; - - public function withMapped(bool $mapped): self; - public function isSortable(): bool; public function withSortable(bool $sortable): self; - public function hasTranslation(): bool; - - public function withTranslation(bool $translation): self; - - public function getTranslationDomain(): ?string; + public function isTranslatable(): bool; - public function withTranslationDomain(?string $translationDomain): self; + public function withTranslatable(bool $translatable): self; public function getAttr(): array; @@ -48,7 +40,7 @@ public function getHeaderAttr(): array; public function withHeaderAttr(array $headerAttr): self; - public function getDataTransformer(): ?DataTransformerInterface; + public function getTranslationDomain(): ?string; - public function withDataTransformer(?DataTransformerInterface $dataTransformer): self; + public function withTranslationDomain(?string $translationDomain): self; } diff --git a/src/Metadata/Property/ResourceLink.php b/src/Metadata/Property/ResourceLink.php new file mode 100644 index 000000000..5cccd2df1 --- /dev/null +++ b/src/Metadata/Property/ResourceLink.php @@ -0,0 +1,70 @@ +resource; + } + + public function withResource(?string $resource): self + { + $self = clone $this; + $self->resource = $resource; + + return $self; + } + + public function getOperation(): ?string + { + return $this->operation; + } + + public function withOperation(?string $operation): self + { + $self = clone $this; + $self->operation = $operation; + + return $self; + } +} diff --git a/src/Metadata/Property/StringProperty.php b/src/Metadata/Property/Text.php similarity index 67% rename from src/Metadata/Property/StringProperty.php rename to src/Metadata/Property/Text.php index 44d8aa18b..e85fe31c5 100644 --- a/src/Metadata/Property/StringProperty.php +++ b/src/Metadata/Property/Text.php @@ -4,39 +4,32 @@ namespace LAG\AdminBundle\Metadata\Property; -use LAG\AdminBundle\Grid\DataTransformer\DataTransformerInterface; - -#[\Attribute] -class StringProperty extends AbstractProperty +class Text extends AbstractProperty { public function __construct( string $name, ?string $propertyPath = null, ?string $label = null, - ?string $template = '@LAGAdmin/grid/properties/string.html.twig', - bool $mapped = true, + ?string $template = '@LAGAdmin/grids/properties/text.html.twig', bool $sortable = true, - bool $translation = false, + bool $translatable = false, ?string $translationDomain = null, array $attr = [], array $headerAttr = [], - ?DataTransformerInterface $dataTransformer = null, private int $length = 100, private string $replace = '...', private string $emptyString = '~', ) { parent::__construct( - $name, - $propertyPath, - $label, - $template, - $mapped, - $sortable, - $translation, - $translationDomain, - $attr, - $headerAttr, - $dataTransformer, + name: $name, + propertyPath: $propertyPath, + label: $label, + template: $template, + sortable: $sortable, + translatable: $translatable, + translationDomain: $translationDomain, + attr: $attr, + headerAttr: $headerAttr, ); } diff --git a/src/Metadata/Property/Title.php b/src/Metadata/Property/Title.php new file mode 100644 index 000000000..d42335b39 --- /dev/null +++ b/src/Metadata/Property/Title.php @@ -0,0 +1,38 @@ +hydrateCache(); + + return $this->cache[$resourceName]; + } + + public function all(): iterable + { + $this->hydrateCache(); + + return $this->cache; + } + + public function has(string $resourceName): bool + { + return $this->decorated->has($resourceName); + } + + private function hydrateCache(): void + { + if ($this->cacheHydrated) { + return; + } + + foreach ($this->decorated->all() as $resource) { + $this->cache[$resource->getName()] = $resource; + } + $this->cacheHydrated = true; + } +} diff --git a/src/Metadata/Registry/ResourceRegistry.php b/src/Metadata/Registry/ResourceRegistry.php new file mode 100644 index 000000000..e09e107ba --- /dev/null +++ b/src/Metadata/Registry/ResourceRegistry.php @@ -0,0 +1,74 @@ + $resourcePaths */ + private array $resourcePaths, + private MetadataLocatorInterface $locator, + private ResourceFactoryInterface $resourceFactory, + ) { + } + + public function get(string $resourceName): AdminResource + { + $this->load(); + + if (!$this->has($resourceName)) { + throw new Exception('Resource with name "'.$resourceName.'" not found'); + } + + return $this->resourceFactory->create($this->definitions[$resourceName]); + } + + public function all(): iterable + { + $this->load(); + + foreach ($this->definitions as $definition) { + yield $this->resourceFactory->create($definition); + } + } + + public function has(string $resourceName): bool + { + return \array_key_exists($resourceName, $this->definitions); + } + + private function load(): void + { + if ($this->loaded) { + return; + } + + foreach ($this->resourcePaths as $path) { + $resources = $this->locator->locateCollection($path); + + foreach ($resources as $resource) { + if (!$resource instanceof AdminResource) { + throw new UnexpectedTypeException($resource, AdminResource::class); + } + + if (!$resource->getName()) { + throw new Exception('The admin resource has no name'); + } + $this->definitions[$resource->getName()] = $resource; + } + } + $this->loaded = true; + } +} diff --git a/src/Metadata/Registry/ResourceRegistryInterface.php b/src/Metadata/Registry/ResourceRegistryInterface.php new file mode 100644 index 000000000..12f7631a3 --- /dev/null +++ b/src/Metadata/Registry/ResourceRegistryInterface.php @@ -0,0 +1,27 @@ + + */ + public function all(): iterable; +} diff --git a/src/Metadata/Show.php b/src/Metadata/Show.php deleted file mode 100644 index dc04b1ce7..000000000 --- a/src/Metadata/Show.php +++ /dev/null @@ -1,59 +0,0 @@ -withResource($resource); + $resource = $resource->withOperations([$operation]); + + $this + ->parametersExtractor + ->expects($this->once()) + ->method('supports') + ->with($request) + ->willReturn(true) + ; + $this + ->parametersExtractor + ->expects($this->once()) + ->method('getResourceName') + ->with($request) + ->willReturn('my_resource') + ; + $this + ->parametersExtractor + ->expects($this->once()) + ->method('getOperationName') + ->with($request) + ->willReturn('my_operation') + ; + + $this + ->resourceRegistry + ->expects($this->once()) + ->method('get') + ->with('my_resource') + ->willReturn($resource) + ; + + $contextResource = $this->resourceContext->getResource($request); + $this->assertEquals($resource->getName(), $contextResource->getName()); + } + + public function testSupports(): void + { + $request = new Request(['test']); + + $this + ->parametersExtractor + ->expects($this->once()) + ->method('supports') + ->with($request) + ->willReturn(true) + ; + + $this->resourceContext->supports($request); + } + + public function testGetWithoutSupport(): void + { + $request = new Request(['test']); + + $this + ->parametersExtractor + ->expects($this->once()) + ->method('supports') + ->with($request) + ->willReturn(false) + ; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('The current request is not supported by any admin resource'); + $this->resourceContext->getOperation($request); + } + + protected function setUp(): void + { + $this->parametersExtractor = $this->createMock(ParametersExtractorInterface::class); + $this->resourceRegistry = $this->createMock(ResourceRegistryInterface::class); + $this->resourceContext = new ResourceContext( + $this->parametersExtractor, + $this->resourceRegistry, + ); + } +} diff --git a/tests/phpunit/Metadata/Factory/OperationFactoryTest.php b/tests/phpunit/Metadata/Factory/OperationFactoryTest.php index a567ec47f..fc3704402 100644 --- a/tests/phpunit/Metadata/Factory/OperationFactoryTest.php +++ b/tests/phpunit/Metadata/Factory/OperationFactoryTest.php @@ -11,8 +11,8 @@ use LAG\AdminBundle\Metadata\Factory\OperationFactory; use LAG\AdminBundle\Metadata\Factory\PropertyFactoryInterface; use LAG\AdminBundle\Metadata\Filter\Filter; -use LAG\AdminBundle\Metadata\Index; -use LAG\AdminBundle\Metadata\Property\StringProperty; +use LAG\AdminBundle\Metadata\GetCollection; +use LAG\AdminBundle\Metadata\Property\Text; use LAG\AdminBundle\Tests\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -26,8 +26,9 @@ class OperationFactoryTest extends TestCase public function testCreate(): void { - $definition = new Index( - properties: [new StringProperty('my_property')], + $definition = new GetCollection( + name: 'get_collection', + properties: [new Text('my_property')], filters: [new Filter('my_filter')], ); $resource = new AdminResource( @@ -46,8 +47,8 @@ public function testCreate(): void OperationEvents::OPERATION_CREATED, 'lag_admin.my_resource.operation.create', 'lag_admin.my_resource.operation.created', - 'lag_admin.my_resource.operation.index.create', - 'lag_admin.my_resource.operation.index.created', + 'lag_admin.my_resource.operation.get_collection.create', + 'lag_admin.my_resource.operation.get_collection.created', ]); return $event; diff --git a/tests/phpunit/Metadata/Factory/PropertyFactoryTest.php b/tests/phpunit/Metadata/Factory/PropertyFactoryTest.php index 489ff4a99..98245f298 100644 --- a/tests/phpunit/Metadata/Factory/PropertyFactoryTest.php +++ b/tests/phpunit/Metadata/Factory/PropertyFactoryTest.php @@ -7,9 +7,9 @@ use LAG\AdminBundle\Exception\Validation\InvalidPropertyCollectionException; use LAG\AdminBundle\Metadata\AdminResource; use LAG\AdminBundle\Metadata\Factory\PropertyFactory; -use LAG\AdminBundle\Metadata\Index; +use LAG\AdminBundle\Metadata\GetCollection; use LAG\AdminBundle\Metadata\Property\PropertyInterface; -use LAG\AdminBundle\Metadata\Property\StringProperty; +use LAG\AdminBundle\Metadata\Property\Text; use LAG\AdminBundle\Tests\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Validator\Constraints\Valid; @@ -23,9 +23,9 @@ class PropertyFactoryTest extends TestCase public function testCreate(): void { - $definition = new StringProperty(name: 'my_property'); - $resource = new AdminResource(name: 'a_resource', translationDomain: 'my_domain'); - $operation = (new Index(properties: [$definition]))->withResource($resource); + $definition = new Text(name: 'my_property'); + $resource = new AdminResource(name: 'a_resource', translationDomain: 'my_domain', applicationName: 'app'); + $operation = (new GetCollection(properties: [$definition]))->withResource($resource); $this ->validator @@ -44,19 +44,20 @@ public function testCreate(): void $this->assertCount(1, $properties); $this->assertArrayHasKey('my_property', $properties); $this->assertEquals('my_property', $properties['my_property']->getName()); - $this->assertEquals('My property', $properties['my_property']->getLabel()); + $this->assertEquals('app.a_resource.my_property', $properties['my_property']->getLabel()); $this->assertEquals('my_domain', $properties['my_property']->getTranslationDomain()); } public function testCreateWithTranslationPattern(): void { - $definition = new StringProperty(name: 'my_property'); + $definition = new Text(name: 'my_property'); $resource = new AdminResource( name: 'a_resource', + applicationName: 'app', translationDomain: 'my_domain', - translationPattern: 'test.{resource}.{property}', + translationPattern: 'test.{resource}.{message}', ); - $operation = (new Index(properties: [$definition]))->withResource($resource); + $operation = (new GetCollection(properties: [$definition]))->withResource($resource); $this ->validator @@ -81,9 +82,9 @@ public function testCreateWithTranslationPattern(): void public function testCreateInvalid(): void { - $definition = new StringProperty(name: 'my_property'); - $resource = new AdminResource(name: 'a_resource', translationDomain: 'my_domain'); - $operation = (new Index(properties: [$definition]))->withResource($resource); + $definition = new Text(name: 'my_property'); + $resource = new AdminResource(applicationName: 'app', name: 'a_resource', translationDomain: 'my_domain'); + $operation = (new GetCollection(properties: [$definition]))->withResource($resource); $violations = $this->createMock(ConstraintViolationList::class); $this diff --git a/tests/phpunit/Metadata/Factory/ResourceFactoryCacheDecoratorTest.php b/tests/phpunit/Metadata/Factory/ResourceFactoryCacheDecoratorTest.php deleted file mode 100644 index a2f2e303c..000000000 --- a/tests/phpunit/Metadata/Factory/ResourceFactoryCacheDecoratorTest.php +++ /dev/null @@ -1,46 +0,0 @@ -decorated - ->expects($this->once()) - ->method('create') - ; - $this->decorator->create($definition); - $this->decorator->create($definition); - } - - public function testCreateWithDifferentName(): void - { - $this - ->decorated - ->expects($this->exactly(2)) - ->method('create') - ; - $this->decorator->create(new AdminResource(name: 'my_resource')); - $this->decorator->create(new AdminResource(name: 'my_other_resource')); - } - - protected function setUp(): void - { - $this->decorated = $this->createMock(ResourceFactoryInterface::class); - $this->decorator = new ResourceFactoryCacheDecorator($this->decorated); - } -} diff --git a/tests/phpunit/Metadata/Factory/ResourceFactoryValidationDecoratorTest.php b/tests/phpunit/Metadata/Factory/ResourceFactoryValidationDecoratorTest.php new file mode 100644 index 000000000..90f5d8d90 --- /dev/null +++ b/tests/phpunit/Metadata/Factory/ResourceFactoryValidationDecoratorTest.php @@ -0,0 +1,76 @@ +decorated + ->expects($this->once()) + ->method('create') + ->with($resource) + ->willReturn($resource) + ; + + $constrainViolations = $this->createMock(ConstraintViolationListInterface::class); + $constrainViolations + ->method('count') + ->willReturn(0) + ; + + $this + ->validator + ->expects($this->exactly(2)) + ->method('validate') + ->willReturnCallback(function (mixed $value, array $constraints) use ($constrainViolations) { + if ($value instanceof AdminResource) { + $this->assertEquals([new AdminValid(), new Valid()], $constraints); + + return $constrainViolations; + } + + if ($value instanceof Operation) { + $this->assertEquals([new Valid()], $constraints); + + return $constrainViolations; + } + + $this->fail(); + }) + ; + + $this->decorator->create($resource); + } + + protected function setUp(): void + { + $this->decorated = $this->createMock(ResourceFactoryInterface::class); + $this->validator = $this->createMock(ValidatorInterface::class); + $this->decorator = new ResourceFactoryValidationDecorator( + $this->validator, + $this->decorated, + ); + } +} diff --git a/tests/phpunit/Metadata/Locator/CompositeFactoryTest.php b/tests/phpunit/Metadata/Locator/CompositeLocatorTest.php similarity index 56% rename from tests/phpunit/Metadata/Locator/CompositeFactoryTest.php rename to tests/phpunit/Metadata/Locator/CompositeLocatorTest.php index 48f222f41..8a351aa67 100644 --- a/tests/phpunit/Metadata/Locator/CompositeFactoryTest.php +++ b/tests/phpunit/Metadata/Locator/CompositeLocatorTest.php @@ -9,8 +9,10 @@ use LAG\AdminBundle\Metadata\Locator\CompositeLocator; use LAG\AdminBundle\Metadata\Locator\MetadataLocatorInterface; use LAG\AdminBundle\Tests\TestCase; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; -class CompositeFactoryTest extends TestCase +class CompositeLocatorTest extends TestCase { public function testCreateResources(): void { @@ -33,16 +35,49 @@ public function testCreateResources(): void ]) ; - $compositeLocator = $this->createLocator([$locator1, $locator2]); + $kernel = $this->createMock(KernelInterface::class); + + $compositeLocator = $this->createLocator([$locator1, $locator2], $this->createMock(KernelInterface::class)); $this->assertEquals([ new AdminResource('an_admin'), new AdminResource('an_other_admin'), ], $compositeLocator->locateCollection('/a/directory')); } + public function testLocateBundleLocators(): void + { + $locator = $this->createMock(MetadataLocatorInterface::class); + $locator + ->expects($this->once()) + ->method('locateCollection') + ->with('/a/path/to/bundle/Entity') + ->willReturn([ + new AdminResource('an_admin'), + ]) + ; + + $bundle = $this->createMock(BundleInterface::class); + $bundle + ->expects($this->once()) + ->method('getPath') + ->willReturn('/a/path/to/bundle') + ; + + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->once()) + ->method('getBundle') + ->with('MyBundle') + ->willReturn($bundle) + ; + + $compositeLocator = $this->createLocator([$locator], $kernel); + $compositeLocator->locateCollection('@MyBundle/Entity'); + } + public function testLocateWithNoLocators(): void { - $compositeLocator = $this->createLocator([]); + $compositeLocator = $this->createLocator([], $this->createMock(KernelInterface::class)); $this->assertEquals([], $compositeLocator->locateCollection('/a/directory')); } @@ -59,12 +94,12 @@ public function testWithWrongLocator(): void ; $this->expectException(Exception::class); - $compositeLocator = $this->createLocator([$wrongLocator]); + $compositeLocator = $this->createLocator([$wrongLocator], $this->createMock(KernelInterface::class)); $compositeLocator->locateCollection('/a/directory'); } - public function createLocator(array $locators): CompositeLocator + public function createLocator(array $locators, KernelInterface $kernel): CompositeLocator { - return new \LAG\AdminBundle\Metadata\Locator\CompositeLocator($locators); + return new CompositeLocator($locators, $kernel); } } diff --git a/tests/phpunit/Metadata/Locator/YamlLocatorTest.php b/tests/phpunit/Metadata/Locator/YamlLocatorTest.php deleted file mode 100644 index 7cd96f90d..000000000 --- a/tests/phpunit/Metadata/Locator/YamlLocatorTest.php +++ /dev/null @@ -1,42 +0,0 @@ -createLocator(); - $resources = $locator->locateCollection(__DIR__.'/../../../app/config/resources/admin'); - - foreach ($resources as $resource) { - $this->assertInstanceOf(AdminResource::class, $resource); - } - $this->assertCount(1, $resources); - } - - public function testLocateWithWrongPath(): void - { - $this->expectException(Exception::class); - $locator = $this->createLocator(); - $locator->locateCollection('/wrong/path'); - } - - public function testService(): void - { - $this->assertServiceExists(YamlLocator::class); - $this->assertServiceHasTag(YamlLocator::class, 'lag_admin.resource.locator'); - } - - private function createLocator(): YamlLocator - { - return new YamlLocator(); - } -} diff --git a/tests/phpunit/Metadata/Registry/ResourceRegistryTest.php b/tests/phpunit/Metadata/Registry/ResourceRegistryTest.php new file mode 100644 index 000000000..6a4678e39 --- /dev/null +++ b/tests/phpunit/Metadata/Registry/ResourceRegistryTest.php @@ -0,0 +1,11 @@ +setValue($object, $value); } - protected function getPrivateProperty($object, $property) + protected function getPrivateProperty(object $object, string $property): mixed { $reflection = new \ReflectionClass($object); $property = $reflection->getProperty($property);