- WIP on request/response handling

This commit is contained in:
Dave M. 2023-10-30 15:21:01 +00:00
parent 5d9c0f0dbf
commit 7e93e57515
10 changed files with 133 additions and 121 deletions

View File

@ -0,0 +1,13 @@
<?php
namespace Ulmus\Api;
use Psr\Http\Message\{ ResponseInterface, RequestInterface };
use Ulmus\Api\Attribute\Obj\Api\ApiAction;
interface ApiHandlerInterface
{
public function handleRequest(RequestInterface $request, ApiAction $attribute) : RequestInterface;
public function handleResponse(ResponseInterface $response, ApiAction $attribute) : ResponseInterface;
}

View File

@ -8,6 +8,7 @@ use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Attribute\Obj\Api\ApiAction;
use Ulmus\Api\Common\MethodEnum;
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Request\JsonRequest;
use Ulmus\Api\Request\Request;
@ -28,56 +29,44 @@ class ApiRepository extends \Ulmus\Repository
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null)
{
parent::__construct($entity, $alias, $adapter);
$this->client = $adapter->adapter()->connect();
}
public function loadOne(): ?object
{
# Must manager URL here !
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Read::class);
$response = $this->executeRequest(Attribute\Obj\Api\Read::class);
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$request = $this->callApiRequestCallback($request, $attribute);
$this->lastResponse = $response = $this->client->fromRequest($request);
$response = $this->callApiResponseCallback($response, $attribute);
return $this->instanciateEntity()->fromArray($response->render());
return $this->instanciateEntity()->fromArray($response->getParsedBody());
}
public function loadAll() : EntityCollection
{
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Collection::class);
$response = $this->executeRequest(Attribute\Obj\Api\Collection::class);
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$request = $this->callApiRequestCallback($request, $attribute);
$this->lastResponse = $response = $this->client->fromRequest($request);
$response = $this->callApiResponseCallback($response, $attribute);
return $this->instanciateEntityCollection()->fromArray($response->render());
return $this->instanciateEntityCollection()->fromArray($response->getParsedBody());
}
public function save(object|array $entity, ?array $fieldsAndValue = null, bool $replace = false): bool
{
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Create::class);
$response = $this->executeRequest(Attribute\Obj\Api\Create::class, $entity->entityLoadedDataset);
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$entity->fromArray($response->getParsedBody());
$this->callApiRequestCallback($request, $attribute);
return $response->getStatusCode() === 200;
}
$this->lastResponse = $response = $this->client->fromRequest($request);
public function executeRequest(string $attributeClass, mixed $data = null) : ResponseInterface
{
$attribute = $this->getApiAttribute($attributeClass);
$ret = $this->callApiResponseCallback($response, $attribute);
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method, $data);
$entity->fromArray($ret);
$request = $this->callApiRequestCallback($request, $attribute);
return true;
$this->lastResponse = $response = $this->adapter->adapter()->connect()->fromRequest($request);
$response = $this->callApiResponseCallback($response, $attribute);
return $response;
}
public function bindUrl(string $parameter, mixed $value) : self
@ -110,9 +99,9 @@ class ApiRepository extends \Ulmus\Repository
}
if ( array_key_exists($variable, $arguments) ) {
$value = $arguments[ $variable ];
$value = $arguments[$variable];
unset($arguments[ $variable ]);
unset($arguments[$variable]);
}
else {
if ($default ?? false) {
@ -132,9 +121,9 @@ class ApiRepository extends \Ulmus\Repository
return $route;
}
protected function prepareRequest(string|UriInterface $uri, MethodEnum $method, null|string $body = null, array $headers = []) : RequestInterface
protected function prepareRequest(string|UriInterface $uri, MethodEnum $method, mixed $body = null, array $headers = []) : RequestInterface
{
return new JsonRequest($uri, $method, $body === null ? Stream::fromTemp() : Stream::fromMemory($body), $headers);
return new JsonRequest($uri, $method, $body === null ? Stream::fromTemp() : JsonStream::fromContent($body), $headers);
}
public function getApiAttribute(string $type) : object
@ -142,22 +131,16 @@ class ApiRepository extends \Ulmus\Repository
return $this->entityClass::resolveEntity()->getAttributeImplementing($type);
}
protected function callApiRequestCallback(RequestInterface $request, ApiAction $attribute) : mixed
protected function callApiRequestCallback(RequestInterface $request, ApiAction $attribute) : RequestInterface
{
return call_user_func_array($this->adapter->apiRequestCallback(), func_get_args());
return $this->adapter->apiHandler->handleRequest($request, $attribute);
}
protected function callApiResponseCallback(ResponseInterface $response, ApiAction $attribute) : mixed
protected function callApiResponseCallback(ResponseInterface $response, ApiAction $attribute) : ResponseInterface
{
return call_user_func_array($this->adapter->apiResponseCallback(), func_get_args());
return $this->adapter->apiHandler->handleResponse($response, $attribute);
}
public function count(): int
{
return 0;
}
public function filterServerRequest(SearchRequestInterface $searchRequest, bool $count = true) : self
{
return $searchRequest->filter($this)
@ -168,4 +151,9 @@ class ApiRepository extends \Ulmus\Repository
->offset($searchRequest->offset())
->limit($searchRequest->limit());
}
public function count(): int
{
return 0;
}
}

View File

@ -7,4 +7,15 @@ enum ContentTypeEnum : string
case Json = "application/json";
case Multipart = "multipart/form-data";
case UrlEncoded = "application/x-www-form-urlencoded";
public function defaultBody() : string
{
switch($this) {
case static::Json:
return "{}";
default:
return "";
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Ulmus\Api\Common;
use Psr\Http\Message\MessageInterface;
use Ulmus\Api\Stream\JsonStream;
trait JsonMessageTrait
{
public function getParsedBody(): mixed
{
return JsonStream::fromJsonEncoded( $this->getBody()->getContents() )->decode();
}
public function withParsedBody(mixed $data) : MessageInterface
{
return (clone $this)->setBody(
JsonStream::fromContent($data)
);
}
}

View File

@ -2,7 +2,10 @@
namespace Ulmus\Api\Common;
use Psr\Http\Message\MessageInterface;
interface StreamOutputInterface
{
public function render() : mixed;
public function getParsedBody() : mixed;
public function withParsedBody(mixed $data) : MessageInterface;
}

View File

@ -2,7 +2,13 @@
namespace Ulmus\Api;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Api\Request\JsonRequest;
use Ulmus\Api\Response\JsonResponse;
use Ulmus\Api\Response\Response;
use Ulmus\Ulmus;
class ConnectionAdapter extends \Ulmus\ConnectionAdapter
{
@ -14,9 +20,15 @@ class ConnectionAdapter extends \Ulmus\ConnectionAdapter
private array $connection;
protected mixed $apiResponseCallback;
public readonly ApiHandlerInterface $apiHandler;
public function __construct(ApiHandlerInterface $apiHandler, $name = "default", array $configuration = [], bool $default = false)
{
parent::__construct($name, $configuration, $default);
$this->apiHandler = $apiHandler;
}
protected mixed $apiRequestCallback;
public function resolveConfiguration(): void
{
@ -64,13 +76,13 @@ class ConnectionAdapter extends \Ulmus\ConnectionAdapter
return new $class();
}
public function apiResponseCallback(callable $callback = null) : callable|null
{
return $callback ? $this->apiResponseCallback = $callback : $this->apiResponseCallback ?? fn($resp) => $resp->render();
}
#public function apiResponseCallback(callable $callback = null) : callable|null
#{
# return $callback ? $this->apiResponseCallback = $callback : $this->apiResponseCallback ?? fn(ResponseInterface $response) => $response;
#}
public function apiRequestCallback(callable $callback = null) : callable|null
{
return $callback ? $this->apiRequestCallback = $callback : $this->apiRequestCallback ?? fn($resp) => $resp->render();
}
#public function apiRequestCallback(callable $callback = null) : callable|null
#{
# return $callback ? $this->apiRequestCallback = $callback : $this->apiRequestCallback ?? fn(RequestInterface $request) => $request;
#}
}

View File

@ -7,52 +7,24 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Common\JsonMessageTrait;
use Ulmus\Api\Common\MethodEnum;
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Common\StreamOutputInterface;
use Ulmus\Api\Common\Uri;
class JsonRequest extends Request implements StreamOutputInterface
{
public const DEFAULT_JSON_ENCODING_FLAGS = JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
use JsonMessageTrait;
public const DEFAULT_JSON_DECODING_FLAGS = JSON_THROW_ON_ERROR;
public int $jsonEncodingOptions;
public function __construct(string|UriInterface $uri, MethodEnum $method, null|StreamInterface $stream = null, array $headers = [], $jsonEncodingOptions = self::DEFAULT_JSON_ENCODING_FLAGS)
public function __construct(string|UriInterface $uri, MethodEnum $method, null|StreamInterface $stream = null, array $headers = [])
{
parent::__construct($uri, $method, $stream, $headers + [ 'Content-Type' => "application/json", 'Accept' => "application/json" ]);
$this->jsonEncodingOptions = $jsonEncodingOptions;
}
public static function fromContent(mixed $content, int $encodingOptions = self::DEFAULT_JSON_ENCODING_FLAGS) : StreamInterface
{
if ( false === $json = json_encode($content, $encodingOptions) ) {
throw new \InvalidArgumentException(sprintf('Json encoding failed with message `%s`.', json_last_error_msg()));
}
return static::fromJsonEncoded($json);
}
public static function fromJsonEncoded(string $json) : StreamInterface
{
$obj = Stream::fromTemp($json);
$obj->rewind();
return $obj;
}
public static function fromRequest(RequestInterface $request) : static
{
return new static($request->getUri(), MethodEnum::from($request->getMethod()), $request->getBody(), $request->getHeaders());
}
public function render(): mixed
{
$content = $this->getBody()->getContents() ?: "[]";
return json_decode($content, true, 1024, static::DEFAULT_JSON_DECODING_FLAGS);
}
}

View File

@ -4,40 +4,21 @@ namespace Ulmus\Api\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Common\JsonMessageTrait;
use Ulmus\Api\Common\MethodEnum;
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Common\StreamOutputInterface;
class JsonResponse extends Response implements StreamOutputInterface
{
public const DEFAULT_JSON_ENCODING_FLAGS = JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
use JsonMessageTrait;
public const DEFAULT_JSON_DECODING_FLAGS = JSON_THROW_ON_ERROR;
public int $jsonEncodingOptions;
public function __construct(null|StreamInterface $stream = null, int $statusCode = 200, array $headers = [], $jsonEncodingOptions = JsonResponse::DEFAULT_JSON_ENCODING_FLAGS)
public function __construct(null|StreamInterface $stream = null, int $statusCode = 200, array $headers = [])
{
parent::__construct($stream, $statusCode, HttpHeaderEnum::normalizeHeaderArray($headers) + [ 'Content-Type' => 'application/json' ]);
$this->jsonEncodingOptions = $jsonEncodingOptions;
}
public static function fromContent(mixed $content, int $encodingOptions = self::DEFAULT_JSON_ENCODING_FLAGS) : StreamInterface
{
if ( false === $json = json_encode($content, $encodingOptions) ) {
throw new \InvalidArgumentException(sprintf('Json encoding failed with message `%s`.', json_last_error_msg()));
}
return static::fromJsonEncoded($json);
}
public static function fromJsonEncoded(string $json) : StreamInterface
{
$obj = Stream::fromTemp($json);
$obj->rewind();
return $obj;
parent::__construct($stream, $statusCode, $headers + [ 'Content-Type' => "application/json" ]);
}
public static function fromResponse(ResponseInterface $response) : static
@ -45,8 +26,8 @@ class JsonResponse extends Response implements StreamOutputInterface
return new static($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
public function render(): mixed
public static function createFrom(mixed $content, int $encodingOptions = JsonStream::DEFAULT_JSON_ENCODING_FLAGS) : ResponseInterface
{
return json_decode($this->getBody()->getContents(), true, 1024, static::DEFAULT_JSON_DECODING_FLAGS);
return new static(JsonStream::fromContent($content, $encodingOptions));
}
}

View File

@ -8,9 +8,9 @@ use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Common\StreamOutputInterface;
class JsonStream extends Stream implements StreamOutputInterface
class JsonStream extends Stream
{
public const DEFAULT_JSON_ENCODING_FLAGS = JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
public const DEFAULT_JSON_ENCODING_FLAGS = JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR;
public const DEFAULT_JSON_DECODING_FLAGS = JSON_THROW_ON_ERROR;
@ -37,13 +37,13 @@ class JsonStream extends Stream implements StreamOutputInterface
public static function fromJsonEncoded(string $json) : StreamInterface
{
$obj = Stream::fromTemp($json);
$obj = static::fromTemp($json);
$obj->rewind();
return $obj;
}
public function render(): mixed
public function decode(): mixed
{
return json_decode($this->getContents(), true, $this->depth, $this->decodingOptions);
}

View File

@ -3,6 +3,7 @@
namespace Ulmus\Api\Transport;
use Ulmus\Api\Common\{ ContentTypeEnum, MethodEnum, HttpHeaderEnum };
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Psr\Http\Message\{ RequestInterface, ResponseInterface };
use Ulmus\Api\Response\{ Response, JsonResponse };
@ -51,7 +52,7 @@ abstract class CurlTransport {
public function fromRequest(RequestInterface $request) : ResponseInterface
{
return $this->request(MethodEnum::from($request->getMethod()), $request->getUri(), $request->getBody()->getContents(), $request->getHeaders());
return $this->request(MethodEnum::from($request->getMethod()), $request->getUri(), $request->getBody(), $request->getHeaders());
}
public function request(MethodEnum $method, string $url, mixed $data = null, array $headers = [], array $options = []) : ResponseInterface
@ -143,7 +144,17 @@ abstract class CurlTransport {
switch($this->contentType) {
case ContentTypeEnum::Json:
$options[CURLOPT_POSTFIELDS] = json_encode(is_string($data) && empty($data) ? new \stdClass() : $data, JsonResponse::DEFAULT_JSON_ENCODING_FLAGS);
if ($data instanceof JsonStream) {
$json = $data->getContents();
}
elseif (is_string($data) && empty($data)) {
$json = $this->contentType->defaultBody();
}
else {
$json = json_encode($data, JsonStream::DEFAULT_JSON_ENCODING_FLAGS);
}
$options[CURLOPT_POSTFIELDS] = $json;
$headers['Accept'] ??= $this->contentType->value;
break;