From ceb342399b9b8ee1058832b9b46304a3b926217c Mon Sep 17 00:00:00 2001 From: Stephan Schuler Date: Mon, 13 Jul 2026 16:09:51 +0200 Subject: [PATCH] feat: Expose model property types and companion classes Model Companions are Repository, Sorting and Filter classers. Model properties types are, in additon to the already existing "attribute", "single relation" and "collectio relation" feature, the value types, which can be used for documentation genration. --- Classes/Controller/GenericModelController.php | 34 +++----- Classes/Domain/Repository/ModelCompanion.php | 86 +++++++++++++++++++ .../GenericModelResourceInformation.php | 2 +- .../Resource/Information/ExposableTypeMap.php | 27 +++++- 4 files changed, 122 insertions(+), 27 deletions(-) create mode 100644 Classes/Domain/Repository/ModelCompanion.php 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