- Still WIP on Api's wrapper

This commit is contained in:
Dave Mc Nicoll 2023-10-27 18:33:33 -04:00
parent 7e69a09e7f
commit 5d9c0f0dbf
16 changed files with 548 additions and 42 deletions

View File

@ -10,6 +10,7 @@
}
],
"require": {
"ext-curl": "*",
"mcnd/ulmus": "dev-master"
},
"repositories": [

View File

@ -47,7 +47,6 @@ class Rest implements AdapterInterface
$this->$conf = $configuration[$conf];
}
}
}
public function writableValue(mixed $value) : mixed

View File

@ -2,10 +2,18 @@
namespace Ulmus\Api;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
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\Stream;
use Ulmus\Api\Request\JsonRequest;
use Ulmus\Api\Request\Request;
use Ulmus\Api\Transport\CurlClient;
use Ulmus\EntityCollection;
use Ulmus\SearchRequest\SearchRequestInterface;
class ApiRepository extends \Ulmus\Repository
{
@ -29,29 +37,41 @@ class ApiRepository extends \Ulmus\Repository
# Must manager URL here !
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Read::class);
$this->lastResponse = $response = $this->client->request($attribute->method, $this->buildRequestUrl($attribute->url));
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$dataset = $this->callApiResponseCallback($response, $attribute);
$request = $this->callApiRequestCallback($request, $attribute);
return $this->instanciateEntity()->fromArray($dataset);
$this->lastResponse = $response = $this->client->fromRequest($request);
$response = $this->callApiResponseCallback($response, $attribute);
return $this->instanciateEntity()->fromArray($response->render());
}
public function loadAll() : EntityCollection
{
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Collection::class);
$this->lastResponse = $response = $this->client->request($attribute->method, $this->buildRequestUrl($attribute->url));
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$collection = $this->callApiResponseCallback($response, $attribute);
$request = $this->callApiRequestCallback($request, $attribute);
return $this->instanciateEntityCollection($collection);
$this->lastResponse = $response = $this->client->fromRequest($request);
$response = $this->callApiResponseCallback($response, $attribute);
return $this->instanciateEntityCollection()->fromArray($response->render());
}
public function save(object|array $entity, ?array $fieldsAndValue = null, bool $replace = false): bool
{
$attribute = $this->getApiAttribute(Attribute\Obj\Api\Create::class);
$this->lastResponse = $response = $this->client->request($attribute->method, $this->buildRequestUrl($attribute->url), $entity->entityLoadedDataset);
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method);
$this->callApiRequestCallback($request, $attribute);
$this->lastResponse = $response = $this->client->fromRequest($request);
$ret = $this->callApiResponseCallback($response, $attribute);
@ -112,13 +132,40 @@ class ApiRepository extends \Ulmus\Repository
return $route;
}
protected function prepareRequest(string|UriInterface $uri, MethodEnum $method, null|string $body = null, array $headers = []) : RequestInterface
{
return new JsonRequest($uri, $method, $body === null ? Stream::fromTemp() : Stream::fromMemory($body), $headers);
}
public function getApiAttribute(string $type) : object
{
return $this->entityClass::resolveEntity()->getAttributeImplementing($type);
}
protected function callApiRequestCallback(RequestInterface $request, ApiAction $attribute) : mixed
{
return call_user_func_array($this->adapter->apiRequestCallback(), func_get_args());
}
protected function callApiResponseCallback(ResponseInterface $response, ApiAction $attribute) : mixed
{
return call_user_func_array($this->adapter->apiResponseCallback(), func_get_args());
}
public function count(): int
{
return 0;
}
public function filterServerRequest(SearchRequestInterface $searchRequest, bool $count = true) : self
{
return $searchRequest->filter($this)
->wheres($searchRequest->wheres(), \Ulmus\Query\Where::OPERATOR_EQUAL, \Ulmus\Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), \Ulmus\Query\Where::CONDITION_OR)
->orders($searchRequest->orders())
->groups($searchRequest->groups())
->offset($searchRequest->offset())
->limit($searchRequest->limit());
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace Ulmus\Api\Response;
namespace Ulmus\Api\Common;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
@ -19,7 +19,7 @@ class Message implements MessageInterface
return $this->protocolVersion;
}
public function withProtocolVersion(string $version) : MessageInterface
public function withProtocolVersion($version) : MessageInterface
{
return (clone $this)->setProtocolVersion($version);
}
@ -47,12 +47,12 @@ class Message implements MessageInterface
return $output;
}
public function hasHeader(string $name) : bool
public function hasHeader($name) : bool
{
return isset($this->headers[HttpHeaderEnum::normalizeHeaderKey($name)]);
}
public function getHeader(string $name) : array
public function getHeader($name) : array
{
$name = HttpHeaderEnum::normalizeHeaderKey($name);
@ -63,7 +63,7 @@ class Message implements MessageInterface
return $this->headers[$name];
}
public function getHeaderLine(string $name) : string
public function getHeaderLine($name) : string
{
$name = HttpHeaderEnum::normalizeHeaderKey($name);
@ -74,17 +74,17 @@ class Message implements MessageInterface
return sprintf("%s: %s", $name, implode(', ', $this->getHeader($name)));
}
public function withHeader(string $name, mixed $value): MessageInterface
public function withHeader($name, mixed $value): MessageInterface
{
return (clone $this)->setHeaders(array_merge_recursive($this->headers, [ HttpHeaderEnum::normalizeHeaderKey($name) => $value ]));
}
public function withAddedHeader(string $name, mixed $value) : MessageInterface
public function withAddedHeader($name, mixed $value) : MessageInterface
{
return (clone $this)->setHeaders([ HttpHeaderEnum::normalizeHeaderKey($name) => $value ] + $this->headers);
}
public function withoutHeader(string $name) : MessageInterface
public function withoutHeader($name) : MessageInterface
{
return (clone $this)->unsetHeader($name);
}

View File

@ -1,6 +1,6 @@
<?php
namespace Ulmus\Api\Request;
namespace Ulmus\Api\Common;
interface StreamOutputInterface
{

241
src/Common/Uri.php Normal file
View File

@ -0,0 +1,241 @@
<?php
namespace Ulmus\Api\Common;
use Psr\Http\Message\UriInterface;
class Uri implements UriInterface
{
public const IGNORE_HTTP_PORT = [ 80, 443 ];
protected string $scheme;
protected null|string $user;
protected null|string $password;
protected string $host;
protected null|int $port;
protected null|string $path;
protected null|array $query;
protected null|string $fragment;
public function __construct(string $uri = null)
{
$this->parse($uri);
}
public static function from(string|UriInterface $uri = "") : UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
return new static($uri);
}
protected function parse(string $uri): void
{
if (false === $parts = parse_url($uri)) {
throw new \InvalidArgumentException(sprintf("Malformed URIs was provided (%s)", $uri));
}
$this->setScheme($parts['scheme'] ?? "");
$this->setUserInfo($parts['user'] ?? null, $parts['pass'] ?? null);
$this->setHost($parts['host'] ?? "");
$this->setPort($parts['port'] ?? null);
$this->setPath($parts['path'] ?? null);
$this->setQuery($parts['query'] ?? []);
$this->setFragment($parts['fragment'] ?? null);
}
public function getScheme()
{
return $this->scheme;
}
public function renderScheme() : string
{
return $this->scheme ? "{$this->scheme}:" : "";
}
protected function setScheme(string $scheme) : self
{
$this->scheme = $scheme;
return $this;
}
public function getAuthority() : string
{
return implode('', array_filter([
$this->getHost(), $this->renderUserInfo(), $this->renderPort(), $this->renderFragment()
]));
}
public function renderAuthority() : string
{
$authority = $this->getAuthority();
return $authority ? "//{$authority}" : $authority;
}
public function getUserInfo() : string
{
return $this->user . ( $this->password ? ":{$this->password}" : "" );
}
public function renderUserInfo() : string
{
return ( $info = $this->getUserInfo() ) ? "{$info}@" : "";
}
public function getHost()
{
return $this->host;
}
protected function setHost(string $host) : static
{
$this->host = $host;
return $this;
}
public function getPort()
{
return $this->port;
}
public function renderPort(array $ignore = self::IGNORE_HTTP_PORT) : string
{
$port = $this->getPort();
return $port && ! in_array($port, $ignore) ? ":{$port}" : "";
}
public function getPath()
{
$path = implode('/', array_map('rawurlencode', explode('/', $this->path)));
return str_starts_with($this->path, '/') ? '/' . ltrim($this->path, '/.') : $this->path;
}
public function renderPath() : string
{
return $this->getPath();
}
public function getQuery()
{
return http_build_query($this->query, "", null, PHP_QUERY_RFC3986);
}
public function renderQuery() : string
{
$query = $this->getQuery();
return $query ? "?{$query}" : "";
}
public function getFragment()
{
return rawurlencode($this->fragment ? "#{$this->fragment}" : "");
}
public function renderFragment() : string
{
return $this->getFragment();
}
public function withScheme($scheme)
{
return (clone $this)->setScheme($scheme);
}
public function withUserInfo($user, $password = null)
{
return (clone $this)->setUserInfo($user, $password);
}
protected function setUserInfo(null|string $user, null|string $password) : static
{
$this->user = $user;
$this->password = $password;
return $this;
}
public function withHost($host)
{
return (clone $this)->setHost($host);
}
public function withPort($port)
{
return (clone $this)->setPort($port);
}
protected function setPort(null|int $port) : static
{
$this->port = $port;
return $this;
}
public function withPath($path)
{
return (clone $this)->setPath($path);
}
protected function setPath(null|string $path) : static
{
$this->path = $path;
return $this;
}
public function withQuery($query)
{
return $this->setQuery($query);
}
protected function setQuery(null|array|string $query) : static
{
if (is_string($query)) {
parse_str($query, $parsed);
}
$this->query = $parsed ?? $query;
return $this;
}
public function withFragment($fragment)
{
return (clone $this)->setFragment($fragment);
}
protected function setFragment(null|string $fragment) : static
{
$this->fragment = $fragment;
return $this;
}
public function __toString()
{
return $this->render();
}
public function render() : string
{
return implode('', array_filter([
$this->renderScheme(), $this->renderAuthority(), $this->renderPath(), $this->renderQuery(), $this->renderFragment()
]));
}
}

View File

@ -16,6 +16,8 @@ class ConnectionAdapter extends \Ulmus\ConnectionAdapter
protected mixed $apiResponseCallback;
protected mixed $apiRequestCallback;
public function resolveConfiguration(): void
{
$connection = $this->configuration['connections'][$this->name] ?? [];
@ -66,4 +68,9 @@ class ConnectionAdapter extends \Ulmus\ConnectionAdapter
{
return $callback ? $this->apiResponseCallback = $callback : $this->apiResponseCallback ?? fn($resp) => $resp->render();
}
public function apiRequestCallback(callable $callback = null) : callable|null
{
return $callback ? $this->apiRequestCallback = $callback : $this->apiRequestCallback ?? fn($resp) => $resp->render();
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Ulmus\Api\Request;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Common\MethodEnum;
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;
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)
{
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);
}
}

104
src/Request/Request.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace Ulmus\Api\Request;
use League\CommonMark\Block\Element\StringContainerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Common\{MethodEnum, Message, Stream, Uri};
class Request extends Message implements RequestInterface
{
protected int $code;
protected string $message;
protected string $target;
protected UriInterface $uri;
protected MethodEnum $method;
public function __construct(string|UriInterface $uri, MethodEnum $method, null|StreamInterface $stream = null, array $headers = [])
{
$this->method = $method;
$this->uri = Uri::from($uri);
$this->stream = $stream ?? Stream::fromMemory();
$this->setHeaders($headers );
}
public function getRequestTarget(): string
{
if ( isset($this->target) ) {
return $this->target;
}
$query = $this->uri->getQuery() ?: null;
$target = $this->uri->getPath() . ( $query ? "?{$query}" : "" );
return $target ?: '/';
}
public function withRequestTarget($requestTarget)
{
return (clone $this)->setRequestTarget($requestTarget);
}
public function setRequestTarget(string $target) : static
{
$this->target = $target;
return $this;
}
public function getMethod()
{
return $this->method->value;
}
public function withMethod($method)
{
return (clone $this)->setMethod($method);
}
protected function setMethod(string $method) : static
{
$this->method = MethodEnum::from($method);
return $this;
}
public function getUri()
{
return (string) $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false)
{
return (clone $this)->setUri($uri, $preserveHost);
}
protected function setUri(UriInterface|string $uri, bool $preserveHost = false) : static
{
$this->uri = Uri::from($uri);
if ( ! $preserveHost || ! $this->hasHeader('Host') ) {
$this->headers['Host'] = [ $this->renderHostHeader() ];
}
return $this;
}
protected function renderHostHeader(UriInterface $fromUri = null) : string
{
$fromUri ??= $this->uri;
$port = $fromUri->getPort();
return implode('', [
$fromUri->getHost(), $port && ! in_array($port, Uri::IGNORE_HTTP_PORT) ? ":{$port}" : ""
]);
}
}

View File

@ -56,13 +56,13 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
public function limit(int $value) : self
{
if ( null === $limit = $this->getFragment(Ulmus\Query\Limit::class) ) {
/*if ( null === $limit = $this->getFragment(Ulmus\Query\Limit::class) ) {
$limit = new Query\Limit();
$this->push($limit);
}
$limit->set($value);
*/
return $this;
}
@ -128,7 +128,7 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
return null;
}
public function removeFragment(Ulmus\Query\Fragment $fragment) : void
public function removeFragment(\Ulmus\Query\Fragment|array|\Stringable|string $fragment) : void
{
foreach($this->queryStack as $key => $item) {
if ( $item === $fragment ) {

View File

@ -5,8 +5,8 @@ namespace Ulmus\Api\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Request\Stream;
use Ulmus\Api\Request\StreamOutputInterface;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Common\StreamOutputInterface;
class JsonResponse extends Response implements StreamOutputInterface
{
@ -34,7 +34,7 @@ class JsonResponse extends Response implements StreamOutputInterface
public static function fromJsonEncoded(string $json) : StreamInterface
{
$obj = static::fromTemp($json);
$obj = Stream::fromTemp($json);
$obj->rewind();
return $obj;

View File

@ -4,7 +4,8 @@ namespace Ulmus\Api\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Ulmus\Api\Request\Stream;
use Ulmus\Api\Common\{ Message };
use Ulmus\Api\Stream\Stream;
class Response extends Message implements ResponseInterface
{
@ -15,7 +16,7 @@ class Response extends Message implements ResponseInterface
public function __construct(null|StreamInterface $stream = null, int $statusCode = 200, array $headers = [])
{
$this->setStatusCode($statusCode);
$this->stream = $stream ?? new Stream("php://memory", "wb+");
$this->stream = $stream ?? Stream::fromMemory();
$this->setHeaders($headers);
}
@ -24,7 +25,7 @@ class Response extends Message implements ResponseInterface
return $this->code;
}
public function withStatus(int $code, null|string $reasonPhrase = null)/* : ResponseInterface*/
public function withStatus($code, $reasonPhrase = null)/* : ResponseInterface*/
{
return (clone $this)->setStatusCode($code, $reasonPhrase);
}

50
src/Stream/JsonStream.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Ulmus\Api\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Ulmus\Api\Common\HttpHeaderEnum;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Common\StreamOutputInterface;
class JsonStream extends Stream implements StreamOutputInterface
{
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_DECODING_FLAGS = JSON_THROW_ON_ERROR;
public int $depth = 1024;
public int $encodingOptions;
public int $decodingOptions;
public function __construct($encodingOptions = self::DEFAULT_JSON_ENCODING_FLAGS, $decodingOptions = self::DEFAULT_JSON_DECODING_FLAGS)
{
$this->encodingOptions = $encodingOptions;
$this->decodingOptions = $decodingOptions;
}
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 function render(): mixed
{
return json_decode($this->getContents(), true, $this->depth, $this->decodingOptions);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace Ulmus\Api\Request;
namespace Ulmus\Api\Stream;
use Psr\Http\Message\StreamInterface;
@ -104,7 +104,7 @@ class Stream implements StreamInterface
public function isWritable() : bool
{
return $this->hasMode('wxca+');
return $this->hasMode('acwx+');
}
public function write($string) : false|int
@ -137,7 +137,7 @@ class Stream implements StreamInterface
return $this->hasMode('r+');
}
public function read(int $length): string
public function read($length): string
{
if (! $this->isReadable()) {
throw new \UnexpectedValueException("An error occured while trying to read a stream which was not readable.");
@ -178,6 +178,6 @@ class Stream implements StreamInterface
$meta = stream_get_meta_data($this->stream);
$mode = $meta['mode'];
return strpbrk($mode, 'acwx+') !== false;
return strpbrk($mode, $search) !== false;
}
}

View File

@ -29,12 +29,12 @@ class CurlAuthorization
$this->basic($username, $password);
break;
case AuthenticationEnum::NTLM:
case AuthenticationEnum::Ntlm:
$this->ntlm($username, $password);
break;
case AuthenticationEnum::Negotiate:
$this->ntlm($username, $password);
$this->negotiate($username, $password);
break;
}

View File

@ -3,12 +3,9 @@
namespace Ulmus\Api\Transport;
use Ulmus\Api\Common\{ ContentTypeEnum, MethodEnum, HttpHeaderEnum };
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\Api\Response\JsonResponse;
use Ulmus\Api\Response\Response;
use Ulmus\Api\Request\Stream;
use Ulmus\Attribute\Obj\Method;
use Ulmus\Api\Stream\Stream;
use Psr\Http\Message\{ RequestInterface, ResponseInterface };
use Ulmus\Api\Response\{ Response, JsonResponse };
abstract class CurlTransport {
@ -52,16 +49,16 @@ abstract class CurlTransport {
return $this->request(MethodEnum::Put, $url, $data, $headers, $curlOptions);
}
public function fromRequest(ServerRequestInterface $request) : ResponseInterface
public function fromRequest(RequestInterface $request) : ResponseInterface
{
return $this->request($request->getMethod(), $request->getUri(), $request->getBody()->getContents(), $request->getHeaders());
return $this->request(MethodEnum::from($request->getMethod()), $request->getUri(), $request->getBody()->getContents(), $request->getHeaders());
}
public function request(MethodEnum $method, string $url, mixed $data = null, array $headers = [], array $options = []) : ResponseInterface
{
$response = new Response();
$headers = HttpHeaderEnum::normalizeHeaderArray($headers + $this->headers);
$headers = array_merge_recursive(HttpHeaderEnum::normalizeHeaderArray($headers), HttpHeaderEnum::normalizeHeaderArray($this->headers));
$options += $this->curlOptions;
@ -83,6 +80,7 @@ abstract class CurlTransport {
CURLOPT_HEADERFUNCTION => function($curl, $headerLine) use (& $response) {
if ( strpos($headerLine, ':') ) {
list($key, $value) = array_map('trim', explode(':', $headerLine));
$response = $response->withHeader($key, $value);
}
elseif ( strtoupper(substr($headerLine, 0, 4)) === 'HTTP' ) {
@ -145,7 +143,7 @@ abstract class CurlTransport {
switch($this->contentType) {
case ContentTypeEnum::Json:
$options[CURLOPT_POSTFIELDS] = json_encode($data, JsonResponse::DEFAULT_JSON_ENCODING_FLAGS);
$options[CURLOPT_POSTFIELDS] = json_encode(is_string($data) && empty($data) ? new \stdClass() : $data, JsonResponse::DEFAULT_JSON_ENCODING_FLAGS);
$headers['Accept'] ??= $this->contentType->value;
break;