diff --git a/Classes/Controller/GenericModelController.php b/Classes/Controller/GenericModelController.php index b109fd3..1a453df 100644 --- a/Classes/Controller/GenericModelController.php +++ b/Classes/Controller/GenericModelController.php @@ -37,6 +37,7 @@ use Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Model\GenericModelInterface; use Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Model\ReadModelInterface; use Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Model\WriteModelInterface; +use Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Repository\ModelCompanion; use Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Repository\GenericModelRepositoryInterface; use Netlogix\JsonApiOrg\AnnotationGenerics\Resource\Information\ExposableTypeMap; use Netlogix\JsonApiOrg\Controller\ApiController; @@ -310,18 +311,11 @@ protected function getRepositoryForResourceType( ->exposableTypeMap ->getExposableTypeByTypeName($resourceType, $apiVersion) ->className; - $parentClasses = array_merge([$class], class_parents($class)); - foreach ($parentClasses as $modelCandidate) { - $repositoryCandidate = str_replace( - '\\Domain\\Model\\', - '\\Domain\\Repository\\', - $modelCandidate - ) . 'Repository'; - if (class_exists($repositoryCandidate)) { - return $this->objectManager->get($repositoryCandidate); - } + $repositoryClassName = ModelCompanion::forModel($class)->repositoryClassName; + if ($repositoryClassName === null) { + throw new UnknownObjectException('No Repository found for class "' . $class . '".', 1264589155); } - throw new UnknownObjectException('No Repository found for class "' . $class . '".', 1264589155); + return $this->objectManager->get($repositoryClassName); } /** @@ -375,6 +369,8 @@ protected function remapActionArguments() ) : null; + $companion = ModelCompanion::forModel($relationshipClassName ?: $exposableType->className); + $this->remapActionArgument( $this->resourceArgumentName, $exposableType->className @@ -382,31 +378,23 @@ protected function remapActionArguments() $this->remapActionArgument( 'sort', - str_replace( - '\\Model\\', - '\\Repository\\Sorting\\', - $relationshipClassName ?: $exposableType->className - ) . 'Sorting' + $companion->sortingClassName ); $this->remapActionArgument( 'filter', - str_replace( - '\\Model\\', - '\\Repository\\Filter\\', - $relationshipClassName ?: $exposableType->className - ) . 'Filter', + $companion->filterClassName, [] ); } - protected function remapActionArgument(string $argumentName, string $modelClassName, $default = null) + protected function remapActionArgument(string $argumentName, ?string $modelClassName, $default = null) { if (!$this->arguments->hasArgument($argumentName)) { return; } - if (false === class_exists($modelClassName)) { + if ($modelClassName === null || false === class_exists($modelClassName)) { return; } diff --git a/Classes/Domain/Repository/ModelCompanion.php b/Classes/Domain/Repository/ModelCompanion.php new file mode 100644 index 0000000..424a1a3 --- /dev/null +++ b/Classes/Domain/Repository/ModelCompanion.php @@ -0,0 +1,86 @@ +sortingClassName + */ +#[Flow\Proxy(false)] +final class ModelCompanion +{ + protected function __construct( + /** + * @var class-string + */ + public readonly string $modelClassName, + /** + * @var class-string + */ + public readonly ?string $filterClassName, + /** + * @var class-string + */ + public readonly ?string $sortingClassName, + /** + * @var class-string + */ + public readonly ?string $repositoryClassName, + ) + { + } + + /** + * @param class-string $modelClassName + * @return self + */ + public static function forModel(string $modelClassName): self + { + $filterClassName = str_replace('\\Model\\', '\\Repository\\Filter\\', $modelClassName) . 'Filter'; + $sortingClassName = str_replace('\\Model\\', '\\Repository\\Sorting\\', $modelClassName) . 'Sorting'; + + return new self( + modelClassName: $modelClassName, + filterClassName: class_exists($filterClassName) ? $filterClassName : null, + sortingClassName: class_exists($sortingClassName) ? $sortingClassName : null, + repositoryClassName: self::resolveRepositoryClassName($modelClassName), + ); + } + + /** + * The Repository is looked up on the model class and its parent classes, + * mirroring GenericModelController::getRepositoryForResourceType(). + */ + private static function resolveRepositoryClassName(string $modelClassName): ?string + { + $modelClassNames = array_values(class_parents($modelClassName) ?: []); + foreach ([$modelClassName, ... $modelClassNames] as $candidate) { + $repositoryClassName = str_replace('\\Domain\\Model\\', '\\Domain\\Repository\\', $candidate) . 'Repository'; + if (class_exists($repositoryClassName)) { + return $repositoryClassName; + } + } + + return null; + } +} diff --git a/Classes/Domain/Resource/GenericModelResourceInformation.php b/Classes/Domain/Resource/GenericModelResourceInformation.php index f357be5..74e5dad 100644 --- a/Classes/Domain/Resource/GenericModelResourceInformation.php +++ b/Classes/Domain/Resource/GenericModelResourceInformation.php @@ -42,7 +42,7 @@ class GenericModelResourceInformation extends ResourceInformation implements Res LinksAwareResourceInformationInterface, MetaAwareResourceInformationInterface { - private const TYPE_NAME_PATTERN = '%^(?[^/]+)/(?.+)$%'; + public const TYPE_NAME_PATTERN = '%^(?[^/]+)/(?.+)$%'; /** * @var int diff --git a/Classes/Resource/Information/ExposableTypeMap.php b/Classes/Resource/Information/ExposableTypeMap.php index d226257..4450c40 100644 --- a/Classes/Resource/Information/ExposableTypeMap.php +++ b/Classes/Resource/Information/ExposableTypeMap.php @@ -56,6 +56,14 @@ class ExposableTypeMap extends BaseExposableTypeMap implements ExposableTypeMapI */ protected $exposableTypeMapCache; + /** + * Property names in their original case, mapped to their resolved PHP + * type string, e.g. 'string' or 'array'. + * + * @var array> + */ + private array $propertyTypesByClassName = []; + /** * All "ExposeType" objects are initialized automatically */ @@ -78,7 +86,7 @@ public function initializeObject() $exposableType = $this->getExposableTypeByClassIdentifier($className); foreach ($properties as $propertyName => $propertyVarType) { try { - $this->registerKnownPropertyType( + $this->propertyTypesByClassName[$className][$propertyName] = $this->registerKnownPropertyType( exposableType: $exposableType, propertyName: $propertyName, varType: $propertyVarType @@ -97,7 +105,7 @@ public function initializeObject() $exposableType = $this->getExposableTypeByClassIdentifier($className); foreach ($methods as $methodName => $methodVarType) { try { - $this->registerKnownPropertyType( + $this->propertyTypesByClassName[$className][$methodName] = $this->registerKnownPropertyType( exposableType: $exposableType, propertyName: $methodName, varType: $methodVarType @@ -113,7 +121,19 @@ public function initializeObject() } } - protected function registerKnownPropertyType(ExposableType $exposableType, string $propertyName, string $varType) + /** + * Public property names (original case) of the given exposed class, + * mapped to their resolved PHP type string, e.g. 'string' or + * 'array'. + * + * @return array + */ + public function getPropertyTypes(string $className): array + { + return $this->propertyTypesByClassName[$className] ?? []; + } + + protected function registerKnownPropertyType(ExposableType $exposableType, string $propertyName, string $varType): string { $varType = TypeHandling::parseType($varType); $isCollection = (bool) $varType['elementType']; @@ -121,6 +141,7 @@ protected function registerKnownPropertyType(ExposableType $exposableType, strin $propertyType = $isCollection ? 'array<' . $elementType . '>' : $elementType; $this->registerExposableTypeProperty($exposableType, strtolower($propertyName), $propertyType); + return $propertyType; } protected static function guessTypeNameFromClassName(PackageManager $packageManager, string $className): string