Skip to content

Commit

Permalink
Merge pull request #3 from sitegeist/task/refactorForLessAssumptions
Browse files Browse the repository at this point in the history
TASK: Refactor for fewer assumptions
  • Loading branch information
mficzel authored Mar 25, 2024
2 parents db9fdcb + 0bb2898 commit b0fa7c8
Show file tree
Hide file tree
Showing 49 changed files with 1,678 additions and 484 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: build

on:
push:
branches:
- 'main'
pull_request:
branches:
- 'main'

jobs:
test:
name: "Test (PHP ${{ matrix.php-versions }}, Flow ${{ matrix.flow-versions }})"

strategy:
fail-fast: false
matrix:
php-versions: ["8.2"]
flow-versions: ["8.3"]
include:
- php-versions: '8.3'
flow-versions: '8.3'

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2
with:
path: ${{ env.FLOW_FOLDER }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite
ini-values: date.timezone="Africa/Tunis", opcache.fast_shutdown=0, apc.enable_cli=on

- name: Set Neos Version
run: composer require neos/flow ^${{ matrix.flow-versions }} --no-progress --no-interaction

- name: Run Linter
run: composer lint

- name: Run Test
run: composer test
12 changes: 8 additions & 4 deletions Classes/Application/OpenApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

namespace Sitegeist\SchemeOnYou\Application;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\ActionResponse;
use Neos\Flow\Mvc\Controller\Arguments;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Mvc\Controller\ControllerInterface;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Sitegeist\SchemeOnYou\Domain\Metadata\PathResponse;
use Sitegeist\SchemeOnYou\Domain\Schema\SchemaNormalizer;

#[Flow\Scope('singleton')]
abstract class OpenApiController implements ControllerInterface
{
protected ActionRequest $request;
Expand All @@ -27,6 +27,7 @@ final public function processRequest(ActionRequest $request, ActionResponse $res
$this->request->setDispatched(true);
$this->response = $response;
$this->response->setContentType('application/json');
$this->response->addHttpHeader('Access-Control-Allow-Origin', '*');
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($this->request);
$this->controllerContext = new ControllerContext(
Expand All @@ -36,7 +37,7 @@ final public function processRequest(ActionRequest $request, ActionResponse $res
$uriBuilder
);

$actionName = $request->getControllerActionName() . 'Endpoint';
$actionName = $request->getControllerActionName() . 'Action';
if (!method_exists($this, $actionName)) {
throw new \DomainException(
'Missing endpoint "' . $request->getControllerActionName() . '" in ' . static::class,
Expand All @@ -48,6 +49,9 @@ final public function processRequest(ActionRequest $request, ActionResponse $res

$result = $this->$actionName(...$parameters);

$this->response->setContent(\json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
$responseMetadata = PathResponse::fromReflectionClass(new \ReflectionClass($result));
$this->response->setStatusCode($responseMetadata->statusCode);

$this->response->setContent(json_encode(SchemaNormalizer::normalizeValue($result), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
}
}
51 changes: 16 additions & 35 deletions Classes/Application/ParameterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@

namespace Sitegeist\SchemeOnYou\Application;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\ObjectManagement\Proxy\ProxyInterface;
use Sitegeist\SchemeOnYou\Domain\Metadata\Parameter as ParameterAttribute;
use Sitegeist\SchemeOnYou\Domain\Metadata\RequestBody;
use Sitegeist\SchemeOnYou\Domain\Metadata\RequestBodyContentType;
use Sitegeist\SchemeOnYou\Domain\Path\ParameterLocation;
use Sitegeist\SchemeOnYou\Domain\Path\RequestParameterContract;
use Sitegeist\SchemeOnYou\Domain\Schema\SchemaDenormalizer;

#[Flow\Proxy(false)]
final readonly class ParameterFactory
class ParameterFactory
{
/**
* @param class-string $className
* @return array<string,RequestParameterContract>
* @return array<string,array<mixed>|object|bool|int|string|float|null>
*/
public static function resolveParameters(string $className, string $methodName, ActionRequest $request): array
{
Expand All @@ -37,36 +33,21 @@ public static function resolveParameters(string $className, string $methodName,
if (!$type instanceof \ReflectionNamedType) {
throw new \DomainException('Can only resolve named parameters with single type', 1709721743);
}
$parameterClassName = $type->getName();
if (!class_exists($parameterClassName)) {
throw new \DomainException('Can only resolve parameters of type class', 1709721783);
if ($type->allowsNull()) {
throw new \DomainException('Nullable types are not supported yet', 1709721755);
}
$parameterReflectionClass = new \ReflectionClass($parameterClassName);
if (!$parameterReflectionClass->implementsInterface(RequestParameterContract::class)) {
throw new \DomainException(
'Can only resolve parameters of type ' . RequestParameterContract::class,
1709722058
);

$parameterAttribute = ParameterAttribute::tryFromReflectionParameter($parameter);
if ($parameterAttribute) {
$parameterValueFromRequest = $parameterAttribute->in->resolveParameterFromRequest($request, $parameter->name);
$parameterValueFromRequest = $parameterAttribute->style->decodeParameterValue($parameterValueFromRequest);
} else {
$requestBodyAttribute = RequestBody::fromReflectionParameter($parameter);
$parameterValueFromRequest = $requestBodyAttribute->contentType->resolveParameterFromRequest($request, $parameter->name);
$parameterValueFromRequest = $requestBodyAttribute->contentType->decodeParameterValue($parameterValueFromRequest);
}
/** @var class-string<RequestParameterContract> $parameterClassName */
$parameters[$parameter->name] = $parameterClassName::fromRequestParameter(
match (ParameterAttribute::tryFromReflectionParameter($parameter)?->in) {
ParameterLocation::LOCATION_PATH => $request->getArgument($parameter->name),
ParameterLocation::LOCATION_QUERY => $request->getHttpRequest()->getQueryParams()[$parameter->name],
ParameterLocation::LOCATION_HEADER => $request->getHttpRequest()->getHeader($parameter->name),
ParameterLocation::LOCATION_COOKIE
=> $request->getHttpRequest()->getCookieParams()[$parameter->name],
null => match (RequestBody::fromReflectionParameter($parameter)->contentType) {
RequestBodyContentType::CONTENT_TYPE_JSON => \json_decode(
(string)$request->getHttpRequest()->getBody(),
true,
512,
JSON_THROW_ON_ERROR
),
RequestBodyContentType::CONTENT_TYPE_FORM => $request->getArgument($parameter->name)
}
}
);

$parameters[$parameter->name] = SchemaDenormalizer::denormalizeValue($parameterValueFromRequest, $type->getName());
}

return $parameters;
Expand Down
32 changes: 0 additions & 32 deletions Classes/Command/OpenApiCommandController.php

This file was deleted.

30 changes: 30 additions & 0 deletions Classes/Command/OpenApiDocumentCommandController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Sitegeist\SchemeOnYou\Command;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;
use Sitegeist\SchemeOnYou\Domain\OpenApiDocumentRepository;

#[Flow\Scope('singleton')]
final class OpenApiDocumentCommandController extends CommandController
{
#[Flow\Inject]
protected OpenApiDocumentRepository $documentRepository;

/**
* @param string $name the name of the api document to render
*/
public function renderCommand(string $name): void
{
$schema = $this->documentRepository->findDocumentByName($name);
$this->output->output(
\json_encode(
$schema,
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
) . PHP_EOL
);
}
}
29 changes: 29 additions & 0 deletions Classes/Controller/OpenApiDocumentController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Sitegeist\SchemeOnYou\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Mvc\View\JsonView;
use Sitegeist\SchemeOnYou\Domain\OpenApiDocumentRepository;

class OpenApiDocumentController extends ActionController
{
protected $defaultViewObjectName = JsonView::class;

#[Flow\Inject]
protected OpenApiDocumentRepository $documentRepository;

public function renderAction(string $name): string
{
$schema = $this->documentRepository->findDocumentByName($name);
$this->response->setContentType('application\json');
$this->response->addHttpHeader('Access-Control-Allow-Origin', '*');
return \json_encode(
$schema,
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
}
}
5 changes: 5 additions & 0 deletions Classes/Domain/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Neos\Flow\Annotations as Flow;
use Sitegeist\SchemeOnYou\Domain\Path\ParameterLocation;
use Sitegeist\SchemeOnYou\Domain\Path\ParameterStyle;

/**
* @see https://swagger.io/specification/#parameter-object
Expand All @@ -14,10 +15,14 @@
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final readonly class Parameter
{
public ParameterStyle $style;

public function __construct(
public ParameterLocation $in,
?ParameterStyle $style = null,
public ?string $description = null,
) {
$this->style = $style ?: ParameterStyle::createDefaultForParameterLocation($this->in);
}

public static function tryFromReflectionParameter(\ReflectionParameter $reflectionParameter): ?self
Expand Down
27 changes: 15 additions & 12 deletions Classes/Domain/Metadata/PathResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@ public function __construct(
public static function fromReflectionClass(\ReflectionClass $reflection): self
{
$pathResponseAttributes = $reflection->getAttributes(self::class);
if (count($pathResponseAttributes) !== 1) {
throw new \DomainException(
'There must be exactly one path response attribute declared in class '
. $reflection->name . ', ' . count($pathResponseAttributes) . ' given',
1709587611
);
switch (count($pathResponseAttributes)) {
case 0:
return new self(200, '');
case 1:
$arguments = $pathResponseAttributes[0]->getArguments();
return new self(
$arguments['statusCode'] ?? $arguments[0],
$arguments['description'] ?? $arguments[1],
);
default:
throw new \DomainException(
'There must be no or exactly one path response attribute declared in class '
. $reflection->name . ', ' . count($pathResponseAttributes) . ' given',
1709587611
);
}
$arguments = $pathResponseAttributes[0]->getArguments();

return new self(
$arguments['statusCode'] ?? $arguments[0],
$arguments['description'] ?? $arguments[1],
);
}
}
30 changes: 30 additions & 0 deletions Classes/Domain/Metadata/RequestBodyContentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,41 @@

namespace Sitegeist\SchemeOnYou\Domain\Metadata;

use Neos\Flow\Mvc\ActionRequest;

enum RequestBodyContentType: string implements \JsonSerializable
{
case CONTENT_TYPE_JSON = 'application/json';
case CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded';

/**
* @todo really?
* @return array<mixed>|int|bool|string|float|null
*/
public function resolveParameterFromRequest(ActionRequest $request, string $parameterName): array|int|bool|string|float|null
{
return match ($this) {
RequestBodyContentType::CONTENT_TYPE_JSON => (string)$request->getHttpRequest()->getBody(),
RequestBodyContentType::CONTENT_TYPE_FORM => $request->getArgument($parameterName)
};
}

/**
* @todo really?
* @param array<mixed>|int|bool|string|float|null $parameterValue
* @return array<mixed>|int|bool|string|float|null
*/
public function decodeParameterValue(array|int|bool|string|float|null $parameterValue): array|int|bool|string|float|null
{
return match ($this) {
self::CONTENT_TYPE_JSON => match (true) {
is_string($parameterValue) => \json_decode($parameterValue, true, 512, JSON_THROW_ON_ERROR),
default => throw new \DomainException('Request body with content type ' . self::CONTENT_TYPE_JSON->value . ' style must be sent as JSON string')
},
default => $parameterValue,
};
}

public function jsonSerialize(): string
{
return $this->value;
Expand Down
24 changes: 14 additions & 10 deletions Classes/Domain/Metadata/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ public function __construct(
public static function fromReflectionClass(\ReflectionClass $reflection): self
{
$definitionReflections = $reflection->getAttributes(Schema::class);
if (count($definitionReflections) !== 1) {
throw new \DomainException(
'There must be exactly one schema attribute declared in class ' . $reflection->name . ', '
. count($definitionReflections) . ' given',
1709537723
if (count($definitionReflections) === 0) {
return new self(
'',
str_replace('\\', '_', $reflection->getName()),
);
} elseif (count($definitionReflections) === 1) {
$arguments = $definitionReflections[0]->getArguments();
return new self(
$arguments['description'] ?? $arguments[0],
$arguments['name'] ?? $arguments[1] ?? str_replace('\\', '_', $reflection->getName()),
);
}
$arguments = $definitionReflections[0]->getArguments();

return new self(
$arguments['description'] ?? $arguments[0],
$arguments['name'] ?? $arguments[1] ?? $reflection->getShortName(),
throw new \DomainException(
'There must be exactly one schema attribute declared in class ' . $reflection->name . ', '
. count($definitionReflections) . ' given',
1709537723
);
}
}
Loading

0 comments on commit b0fa7c8

Please sign in to comment.