Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 11 additions & 23 deletions Classes/Controller/GenericModelController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -375,38 +369,32 @@ protected function remapActionArguments()
)
: null;

$companion = ModelCompanion::forModel($relationshipClassName ?: $exposableType->className);

$this->remapActionArgument(
$this->resourceArgumentName,
$exposableType->className
);

$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;
}

Expand Down
86 changes: 86 additions & 0 deletions Classes/Domain/Repository/ModelCompanion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Netlogix\JsonApiOrg\AnnotationGenerics\Domain\Repository;

/*
* This file is part of the Netlogix.JsonApiOrg.AnnotationGenerics package.
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use function array_values;
use function class_exists;
use function class_parents;
use function str_replace;

/**
* Resolves the conventional companion classes of a Domain\Model class:
* its Filter, Sorting and Repository. The named constructor checks whether
* each companion actually exists; the readonly properties hold the existing
* class name or null.
*
* Usage: ModelCompanion::forModel($modelClassName)->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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class GenericModelResourceInformation extends ResourceInformation implements Res
LinksAwareResourceInformationInterface,
MetaAwareResourceInformationInterface
{
private const TYPE_NAME_PATTERN = '%^(?<subPackage>[^/]+)/(?<resourceType>.+)$%';
public const TYPE_NAME_PATTERN = '%^(?<subPackage>[^/]+)/(?<resourceType>.+)$%';

/**
* @var int
Expand Down
27 changes: 24 additions & 3 deletions Classes/Resource/Information/ExposableTypeMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vendor\Package\Domain\Model\Foo>'.
*
* @var array<string, array<string, string>>
*/
private array $propertyTypesByClassName = [];

/**
* All "ExposeType" objects are initialized automatically
*/
Expand All @@ -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
Expand All @@ -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
Expand All @@ -113,14 +121,27 @@ 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<Vendor\Package\Domain\Model\Foo>'.
*
* @return array<string, string>
*/
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'];
$elementType = $isCollection ? $varType['elementType'] : $varType['type'];

$propertyType = $isCollection ? 'array<' . $elementType . '>' : $elementType;
$this->registerExposableTypeProperty($exposableType, strtolower($propertyName), $propertyType);
return $propertyType;
}

protected static function guessTypeNameFromClassName(PackageManager $packageManager, string $className): string
Expand Down