diff --git a/src/ApiHandlerInterface.php b/src/ApiHandlerInterface.php new file mode 100644 index 0000000..d3bd9a9 --- /dev/null +++ b/src/ApiHandlerInterface.php @@ -0,0 +1,13 @@ +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; + } } \ No newline at end of file diff --git a/src/Common/ContentTypeEnum.php b/src/Common/ContentTypeEnum.php index 1ee1931..52d8934 100644 --- a/src/Common/ContentTypeEnum.php +++ b/src/Common/ContentTypeEnum.php @@ -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 ""; + } + } } diff --git a/src/Common/JsonMessageTrait.php b/src/Common/JsonMessageTrait.php new file mode 100644 index 0000000..a249b8e --- /dev/null +++ b/src/Common/JsonMessageTrait.php @@ -0,0 +1,21 @@ +getBody()->getContents() )->decode(); + } + + public function withParsedBody(mixed $data) : MessageInterface + { + return (clone $this)->setBody( + JsonStream::fromContent($data) + ); + } +} \ No newline at end of file diff --git a/src/Common/StreamOutputInterface.php b/src/Common/StreamOutputInterface.php index 25af89f..3b1baf0 100644 --- a/src/Common/StreamOutputInterface.php +++ b/src/Common/StreamOutputInterface.php @@ -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; } \ No newline at end of file diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index 29e0138..e16c61a 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -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; + #} } diff --git a/src/Request/JsonRequest.php b/src/Request/JsonRequest.php index 6185fc8..a22a56b 100644 --- a/src/Request/JsonRequest.php +++ b/src/Request/JsonRequest.php @@ -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); - } } \ No newline at end of file diff --git a/src/Response/JsonResponse.php b/src/Response/JsonResponse.php index cb9f660..6e183ca 100644 --- a/src/Response/JsonResponse.php +++ b/src/Response/JsonResponse.php @@ -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)); } } \ No newline at end of file diff --git a/src/Stream/JsonStream.php b/src/Stream/JsonStream.php index dfcacb2..fa5e57f 100644 --- a/src/Stream/JsonStream.php +++ b/src/Stream/JsonStream.php @@ -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); } diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index e2cb9e7..de621f4 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -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;