Compare commits

...
17 Commits
Author SHA1 Message Date
Dave Mc Nicoll 3ddd8a399a - PHP 8.5 don't support abstract class being attributes, removed from ApiAction 2026-10-22 19:42:47 +00:00
mcndave 042ff5cc2f - WIP on PHP 8.5 support 2026-07-14 15:53:21 +00:00
mcndave fe56a1727b - Added some more features to attributes and various Curl wrappers 2026-07-01 13:24:04 -04:00
Dave Mc Nicoll b1adf5a086 - Fixed some bugs regarding parameter binding 2026-06-15 19:36:09 +00:00
Dave Mc Nicoll 0550036fd9 - Added generateArray method for Enums 2025-05-21 18:36:46 +00:00
Dave Mc Nicoll 2d6f2c665b - WIP on destroy() method for an API DELET call 2025-02-04 08:46:27 -05:00
Dave Mc Nicoll 6d7701cfe5 - Added some more functionnality to API's connector 2024-12-02 15:24:26 -05:00
Dave Mc Nicoll 98c608dfba - Added missing escapeIdentifier() from the Adapter 2024-11-13 14:07:14 -05:00
Dave Mc Nicoll e945a7a9b2 - Fixed a bug within ApiRepository with MethodENum 2024-06-03 21:51:43 -04:00
mcndave fa1d4783d2 Fixed an important bug where request callback was never applied to request ! 2024-06-04 01:49:20 +00:00
Dave Mc Nicoll 7a31baa586 - Quickfix on missing debugResponse method of Apihandler 2024-04-24 11:23:43 -04:00
Dave Mc Nicoll 4e120cea6c - Added SearchRequest compatibilty and began working on QueryBuilder processing 2024-04-17 15:57:51 -04:00
Dave Mc Nicoll ce4d6cdf7c - Added Oauth2 authentication. Fixed non-working bearer token also. 2024-04-09 19:13:53 -04:00
mcndave f035fdf0a2 - Timeout now set at 30 sec like CURL is. 2024-02-26 11:14:04 -05:00
Dave Mc Nicoll 56ca1e2071 - Fixed a bug occuring within Timeout 2024-02-16 14:38:18 -05:00
Dave Mc Nicoll aba0ec3458 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus-api 2023-11-17 16:02:05 -05:00
Dave Mc Nicoll c026762fdb - Fixed ternary where a false value was provided whenever the env var was empty 2023-11-17 16:01:54 -05:00
32 changed files with 811 additions and 191 deletions

No files matched your search

+40 -19
View File
@@ -2,6 +2,7 @@
namespace Ulmus\Api\Adapter;
use Negundo\Client\Transport\Curl;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Api\ApiRepository;
use Ulmus\Api\Common\AuthenticationEnum;
@@ -13,23 +14,35 @@ use Ulmus\Ulmus;
class Rest implements AdapterInterface
{
protected AuthenticationEnum $auth;
protected CurlAuthorization $authorization;
protected string $username;
protected string $password;
protected string $token;
protected AuthenticationEnum $authorizationMethod;
protected array $headers = [];
protected array $options = [];
protected array $parameters = [];
public readonly string $url;
public function setup(array $configuration): void
{
$this->url = rtrim($configuration['url'], '/');
$this->authorizationMethod = AuthenticationEnum::from($configuration['auth'] ?: AuthenticationEnum::Basic->value);
$this->authorization = new CurlAuthorization($configuration);
$this->headers = $configuration['headers'] ?? [];
$this->options = $configuration['options'] ?? [];
$this->parameters = $configuration['parameters'] ?? [];
}
public function connect(): object
{
return new CurlClient($this->headers, $this->options, ( new CurlAuthorization() )->fromAuthenticationEnum($this->auth, $this->username ?? false, $this->password ?? false, $this->token ?? false) );
$this->authorization = $this->authorization->fromAuthenticationEnum($this->authorizationMethod);
return new CurlClient([], $this->options, $this->authorization);
}
public function buildDataSourceName(): string
@@ -37,18 +50,6 @@ class Rest implements AdapterInterface
return "https";
}
public function setup(array $configuration): void
{
$this->auth = AuthenticationEnum::from($configuration['auth'] ?? AuthenticationEnum::Basic->value);
$this->url = rtrim($configuration['url'], '/');
foreach([ 'username', 'password', 'token', 'headers', 'options' ] as $conf) {
if ($configuration[$conf] ?? false) {
$this->$conf = $configuration[$conf];
}
}
}
public function writableValue(mixed $value) : mixed
{
switch (true) {
@@ -77,4 +78,24 @@ class Rest implements AdapterInterface
{
return RequestBuilder::class;
}
public function getCurlOptions() : array
{
return $this->options;
}
public function getHeaders() : array
{
return $this->headers;
}
public function getParameters() : array
{
return $this->parameters;
}
public function escapeIdentifier(string $segment, int $type): string
{
return $segment;
}
}
+163 -34
View File
@@ -4,17 +4,21 @@ namespace Ulmus\Api;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Ulmus\Api\Attribute\Obj\Api\ApiAction;
use Ulmus\Api\Common\MethodEnum;
use Ulmus\Api\RequestBuilder\Filter;
use Ulmus\Api\RequestBuilder\UrlParameter;
use Ulmus\Api\SearchRequest\ApiSearchRequest;
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Ulmus\Api\Request\JsonRequest;
use Ulmus\Api\Request\Request;
use Ulmus\Api\Transport\CurlClient;
use Ulmus\Api\Transport\CurlTransport;
use Ulmus\Entity\EntityInterface;
use Ulmus\EntityCollection;
use Ulmus\SearchRequest\SearchRequestInterface;
use Ulmus\Ulmus;
class ApiRepository extends \Ulmus\Repository
{
@@ -22,34 +26,62 @@ class ApiRepository extends \Ulmus\Repository
public CurlClient $client;
protected array $urlParameters = [];
public ResponseInterface $lastResponse;
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null)
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, null|ConnectionAdapter $adapter = null)
{
parent::__construct($entity, $alias, $adapter);
}
public function loadOne(): ? object
{
$response = $this->executeRequest(Attribute\Obj\Api\Read::class);
return $this->loadOneFromAttribute(Attribute\Obj\Api\Read::class);
}
return $this->instanciateEntity()->fromArray($response->getParsedBody());
public function loadOneFromAttribute(string $attributeClass): ? object
{
return $this->instanciateEntity(null, $this->executeRequest($attributeClass)->getParsedBody());
}
public function loadAll() : EntityCollection
{
$response = $this->executeRequest(Attribute\Obj\Api\Collection::class);
return $this->loadAllFromAttribute(Attribute\Obj\Api\Collection::class);
}
public function loadAllFromAttribute(string $attributeClass) : EntityCollection
{
$response = $this->executeRequest($attributeClass);
return $this->instanciateEntityCollection()->fromArray($response->getParsedBody());
}
public function search() : EntityCollection
{
return $this->loadAllFromAttribute(Attribute\Obj\Api\Search::class);
}
public function save(object|array $entity, ?array $fieldsAndValue = null, bool $replace = false): bool
{
$response = $this->executeRequest(Attribute\Obj\Api\Create::class, $entity->entityGetDataset());
if ($replace && $entity instanceof EntityInterface) {
$entity->fromArray($response->getParsedBody());
}
return $response->getStatusCode() === 200;
}
public function destroy(object $entity) : bool
{
if ( ! $this->matchEntity($entity) ) {
throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`");
}
$this->wherePrimaryKeyValue($entity);
$this->eventExecute(\Ulmus\Event\Query\Delete::class, $this, $entity);
$response = $this->executeRequest(Attribute\Obj\Api\Delete::class);
return $response->getStatusCode() === 200;
}
@@ -62,33 +94,39 @@ class ApiRepository extends \Ulmus\Repository
throw new \RuntimeException(sprintf("Could not find attribute class '%s' for class '%s'", $attributeClass, $this->entityClass));
}
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method, $data);
if (method_exists($this->adapter->apiHandler, 'executingRequest')) {
$this->adapter->apiHandler->executingRequest($this, $attribute);
}
$request = $this->prepareRequest($this->buildRequestUrl($attribute->url), $attribute->method, $data, $this->adapter->adapter()->getHeaders(), $this->adapter->adapter()->getParameters());
$request = $this->compileRequestParameters($request, $attribute);
$request = $this->callApiRequestCallback($request, $attribute);
$rest = $this->adapter->adapter()->connect();
$rest->timeout = $attribute->timeout;
$this->lastResponse = $response = $rest->fromRequest($request);
$response = $this->launchRequest($request, $attribute);
$response = $this->callApiResponseCallback($response, $attribute);
return $response;
}
public function bindUrl(string $parameter, mixed $value) : self
protected function buildRequestUrl(string $uri) : string
{
$this->urlParameters[$parameter] = $value;
return $this;
return $this->adapter->adapter()->url . '/' . ltrim($uri, '/');
}
protected function buildRequestUrl(string $uri) :string
protected function launchRequest(RequestInterface $request, object $attribute) : ResponseInterface
{
$uri = $this->prepareUri($uri, $this->urlParameters);
$transport = $this->adapter->adapter()->connect();
return $this->adapter->adapter()->url . '/' . ltrim($uri, '/');
$transport->timeout = $attribute->timeout;
$this->lastResponse = $transport->fromRequest($request);
$this->callApiDebugCallback($transport);
return $this->lastResponse;
}
protected function prepareUri(string $route, array $arguments) : string
@@ -129,9 +167,47 @@ class ApiRepository extends \Ulmus\Repository
return $route;
}
protected function prepareRequest(string|UriInterface $uri, MethodEnum $method, mixed $body = null, array $headers = []) : RequestInterface
protected function prepareRequest(string|UriInterface $uri, MethodEnum $method, mixed $body = null, array $headers = [], array $queryParameters = []) : ServerRequestInterface
{
return new JsonRequest($uri, $method, $body === null ? Stream::fromTemp() : JsonStream::fromContent($body), $headers);
$request = new JsonRequest($uri, $method, $body === null ? Stream::fromTemp() : JsonStream::fromContent($body), $headers);
# Adding parameters
$request = $request->withQueryParams($queryParameters)
->withAttribute('repository', $this);
return $request;
}
protected function compileRequestParameters(ServerRequestInterface $request, object $attribute) : ServerRequestInterface
{
$requestOptions = $this->queryBuilder->render();
$request = $this->applyFiltering($request, $attribute, $requestOptions[Filter::KEY] ?? []);
$request = $this->applyUrlBinding($request, $attribute, $requestOptions[UrlParameter::KEY] ?? []);
return $request;
}
protected function applyUrlBinding(ServerRequestInterface $request, object $attribute, array $bindings) : ServerRequestInterface
{
$uriObj = $request->getUri();
$uri = $this->prepareUri($request->getUri()->render(), $bindings);
return $request->withUri($uriObj->from($uri));
}
protected function applyFiltering(ServerRequestInterface $request, object $attribute, array $filters) : ServerRequestInterface
{
if ($attribute->searchMethod === MethodEnum::Get) {
$request = $request->withQueryParams($filters);
}
elseif ($attribute->searchMethod === MethodEnum::Post) {
$request = $request->withParsedBody($filters);
}
return $request;
}
public function getApiAttribute(string $type) : ? object
@@ -139,29 +215,82 @@ class ApiRepository extends \Ulmus\Repository
return $this->entityClass::resolveEntity()->getAttributeImplementing($type);
}
protected function callApiRequestCallback(RequestInterface $request, ApiAction $attribute) : RequestInterface
protected function callApiRequestCallback(RequestInterface $request, Attribute\Obj\Api\ApiAction $attribute) : ServerRequestInterface
{
return $this->adapter->apiHandler->handleRequest($request, $attribute);
}
protected function callApiResponseCallback(ResponseInterface $response, ApiAction $attribute) : ResponseInterface
protected function callApiResponseCallback(ResponseInterface $response, Attribute\Obj\Api\ApiAction $attribute) : ResponseInterface
{
return $this->adapter->apiHandler->handleResponse($response, $attribute);
}
public function filterServerRequest(SearchRequestInterface $searchRequest, bool $count = true) : self
protected function callApiDebugCallback(CurlTransport $transport) : void
{
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());
if (method_exists($this->adapter->apiHandler, 'debugResponse')) {
$this->adapter->apiHandler->debugResponse($transport, $this);
}
}
public function filterServerRequest(SearchRequestInterface $searchRequest, bool $count = true) : \Ulmus\Repository
{
if ($searchRequest instanceof ApiSearchRequest) {
$this->bindings($searchRequest->bindings());
}
return parent::filterServerRequest($searchRequest, false);
}
public function count(): int
{
return 0;
}
public function bindUrl(string|\Stringable $field, mixed $value) : self
{
$this->queryBuilder->bindUrl($field, $value);
return $this;
}
public function bindings(array $fieldValues) : self
{
foreach($fieldValues as $field => $value) {
$this->bindUrl($field, $value);
}
return $this;
}
public function wherePrimaryKey(mixed $value, null|string|bool $alias = self::DEFAULT_ALIAS) : self
{
$primaryKeyField = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if ( $primaryKeyField === null ) {
throw new \Ulmus\Exception\EntityPrimaryKeyUnknown(sprintf("No primary key found for entity %s", $this->entityClass));
}
$pkField = key($primaryKeyField);
return $this->where($pkField, $value);
}
public function wherePrimaryKeyValue(EntityInterface $entity) : self
{
$primaryKeyField = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if ( $primaryKeyField === null ) {
throw new \Ulmus\Exception\EntityPrimaryKeyUnknown(sprintf("No primary key found for entity %s", $this->entityClass));
}
$pkField = key($primaryKeyField);
$value = $entity->$pkField;
if ( $value !== 0 && empty($value) ) {
throw new \Ulmus\Exception\EntityPrimaryKeyUnknown(sprintf("A primary key value has to be defined to delete an item of entity %s.", $entity::class));
}
return $this->where($pkField, $value);
}
}
+4 -2
View File
@@ -5,11 +5,13 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute]
abstract class ApiAction
class ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method,
public int $timeout = 5,
public int $timeout = 30,
# Define how the search string will be build, passing params through the URL or into request's body
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,9 +5,12 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Collection extends ApiAction {
class Collection extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Get,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,9 +5,12 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Create extends ApiAction {
class Create extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Post,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,9 +5,12 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Delete extends ApiAction {
class Delete extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Delete,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,9 +5,12 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Read extends ApiAction {
class Read extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Get,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Search extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Get,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,9 +5,12 @@ namespace Ulmus\Api\Attribute\Obj\Api;
use Ulmus\Api\Common\MethodEnum;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Update extends ApiAction {
class Update extends ApiAction
{
public function __construct(
public string $url,
public MethodEnum $method = MethodEnum::Patch,
public int $timeout = 30,
public MethodEnum $searchMethod = MethodEnum::Get,
) {}
}
+4 -1
View File
@@ -5,11 +5,12 @@ namespace Ulmus\Api\Common;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Ulmus\Api\ApiHandlerInterface;
use Ulmus\Api\ApiRepository;
use Ulmus\Api\Attribute\Obj\Api\ApiAction;
use Ulmus\Api\Transport\CurlTransport;
class ApiHandlerPassThrough implements ApiHandlerInterface
{
public function handleRequest(RequestInterface $request, ApiAction $attribute): RequestInterface
{
return $request;
@@ -19,4 +20,6 @@ class ApiHandlerPassThrough implements ApiHandlerInterface
{
return $response;
}
public function debugResponse(CurlTransport $transport, ApiRepository $repository) : void {}
}
+1 -1
View File
@@ -11,7 +11,7 @@ enum AuthenticationEnum : string
case Key = "key";
case Ntlm = "ntlm";
case Negotiate = "negotiate";
# case OAuth = "oauth";
case OAuth2 = "oauth2";
# case OpenID = "openid";
# case OpenAPI = "openapi";
case Token = "token";
+3 -1
View File
@@ -5,8 +5,10 @@ namespace Ulmus\Api\Common;
enum ContentTypeEnum : string
{
case Json = "application/json";
case Multipart = "multipart/form-data";
case Xml = "application/xml";
case RssXml = "application/rss+xml";
case UrlEncoded = "application/x-www-form-urlencoded";
case Multipart = "multipart/form-data";
public function defaultBody() : string
{
+2 -2
View File
@@ -7,12 +7,12 @@ use Ulmus\Api\Stream\JsonStream;
trait JsonMessageTrait
{
public function getParsedBody(): mixed
public function getParsedBody() : null|array|object
{
return JsonStream::fromJsonEncoded( $this->getBody()->getContents() )->decode() ;
}
public function withParsedBody(mixed $data) : MessageInterface
public function withParsedBody(mixed $data) : static
{
return (clone $this)->setBody(
JsonStream::fromContent($data)
+7
View File
@@ -13,4 +13,11 @@ enum MethodEnum : string
case Post = "POST"; # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST )
case Put = "PUT"; # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT )
case Trace = "TRACE"; # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE )
public static function generateArray() : array
{
$values = array_map(fn($e) => $e->value, MethodEnum::cases());
return array_combine($values, $values);
}
}
+1 -1
View File
@@ -7,5 +7,5 @@ use Psr\Http\Message\MessageInterface;
interface StreamOutputInterface
{
public function getParsedBody() : mixed;
public function withParsedBody(mixed $data) : MessageInterface;
public function withParsedBody(mixed $data) : static;
}
+1 -1
View File
@@ -24,7 +24,7 @@ class Uri implements UriInterface
protected null|string $fragment;
public function __construct(string $uri = null)
public function __construct(?string $uri = null)
{
$this->parse($uri);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace Ulmus\Api\Common;
use Psr\Http\Message\MessageInterface;
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\XmlStream;
# @TODO !
trait XmlMessageTrait
{
public function getParsedBody() : null|array|object
{
return XmlStream::fromXmlEncoded( $this->getBody()->getContents() )->decode() ;
}
public function withParsedBody(mixed $data) : static
{
return (clone $this)->setBody(
XmlStream::fromContent($data)
);
}
}
+13 -22
View File
@@ -4,6 +4,7 @@ namespace Ulmus\Api;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Api\Request\JsonRequest;
use Ulmus\Api\Response\JsonResponse;
@@ -12,24 +13,20 @@ use Ulmus\Ulmus;
class ConnectionAdapter extends \Ulmus\ConnectionAdapter
{
public string $name;
public array $configuration;
protected AdapterInterface $adapter;
private array $connection;
public readonly ApiHandlerInterface $apiHandler;
public function __construct(ApiHandlerInterface $apiHandler, $name = "default", array $configuration = [], bool $default = false)
{
public function __construct(
public readonly ApiHandlerInterface $apiHandler,
public string $name = "default",
protected array $configuration = [],
public bool $default = false,
public ? CacheInterface $cacheObject = null
) {
parent::__construct($name, $configuration, $default);
$this->apiHandler = $apiHandler;
}
public function resolveConfiguration(): void
{
$connection = $this->configuration['connections'][$this->name] ?? [];
@@ -71,18 +68,12 @@ class ConnectionAdapter extends \Ulmus\ConnectionAdapter
protected function instanciateAdapter($name): AdapterInterface
{
$class = substr($name, 0, 2) === "\\" ? $name : "\\Ulmus\\Api\\Adapter\\$name";
if (class_exists($name)) {
return new $name();
}
$class = sprintf("\\%s\\Adapter\\%s", __NAMESPACE__, $name);
return new $class();
}
#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(RequestInterface $request) => $request;
#}
}
+127 -8
View File
@@ -2,13 +2,11 @@
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};
use Ulmus\Api\Common\{MethodEnum, Message, Uri, };
use Ulmus\Api\Stream\{JsonStream, Stream};
use Psr\Http\Message\{MessageInterface, ServerRequestInterface, UriInterface, RequestInterface, StreamInterface};
class Request extends Message implements RequestInterface
class Request extends Message implements ServerRequestInterface
{
protected int $code;
@@ -20,6 +18,12 @@ class Request extends Message implements RequestInterface
protected MethodEnum $method;
protected array $cookies = [];
protected array $files = [];
protected array $attributes = [];
public function __construct(string|UriInterface $uri, MethodEnum $method, null|StreamInterface $stream = null, array $headers = [])
{
$this->method = $method;
@@ -70,9 +74,9 @@ class Request extends Message implements RequestInterface
return $this;
}
public function getUri()
public function getUri() : UriInterface
{
return (string) $this->uri;
return $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false)
@@ -101,4 +105,119 @@ class Request extends Message implements RequestInterface
$fromUri->getHost(), $port && ! in_array($port, Uri::IGNORE_HTTP_PORT) ? ":{$port}" : ""
]);
}
public function withQueryParams(array $query) : static
{
return (clone $this)->addQueryParams($query);
}
protected function addQueryParams(array $query) : static
{
$rebuilt = http_build_query(array_merge($this->getQueryParams(), $query), "", null, PHP_QUERY_RFC3986);
$this->getUri()->withQuery($rebuilt);
return $this;
}
public function getServerParams() : array
{
return [];
}
public function getCookieParams() : array
{
return $this->cookies;
}
public function withCookieParams(array $cookies) : static
{
return (clone $this)->setCookies($cookies);
}
protected function setCookies(array $cookies) : static
{
$this->cookies = $cookies;
return $this;
}
public function getQueryParams() : array
{
parse_str($this->getUri()->getQuery(), $query);
return $query;
}
public function getUploadedFiles() : array
{
return $this->files;
}
public function withUploadedFiles(array $uploadedFiles) : static
{
return (clone $this)->setUploadedFiles($uploadedFiles);
}
protected function setUploadedFiles(array $uploadedFiles) : static
{
$this->files = $uploadedFiles;
return $this;
}
public function getParsedBody() : null|array|object
{
$raw = $this->getBody()->getContents();
$contentType = $this->getHeader('Content-Type')[0] ?? "application/x-www-form-urlencoded";
switch($contentType) {
case "application/x-www-form-urlencoded":
parse_str($raw, $result);
return $result;
case "multipart/form-data":
return [];
case "application/json":
return JsonStream::fromJsonEncoded( $raw )->decode();
}
return [ sprintf("Unsupported content type: %s", $contentType) ];
}
public function withParsedBody(mixed $data) : static
{
return (clone $this)->setBody(
JsonStream::fromContent($data)
);
}
public function getAttributes() : array
{
return $this->attributes;
}
public function getAttribute($name, $default = null) : mixed
{
return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
public function withAttribute($name, $value) : static
{
$this->attributes[$name] = $value;
return $this;
}
public function withoutAttribute($name): static
{
unset($this->attributes[$name]);
return $this;
}
}
+51 -74
View File
@@ -2,23 +2,25 @@
namespace Ulmus\Api;
use Ulmus;
use Ulmus, Ulmus\Query\QueryFragmentInterface;
class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
class RequestBuilder implements Ulmus\QueryBuilder\QueryBuilderInterface
{
public Ulmus\Query\QueryBuilderInterface $parent;
public Ulmus\QueryBuilder\QueryBuilderInterface $parent;
public array $parameters = [];
public RequestBuilder\Filter $filters;
public array $values = [];
public string $whereConditionOperator = Ulmus\Query\Where::CONDITION_AND;
public string $havingConditionOperator = Ulmus\Query\Where::CONDITION_AND;
public RequestBuilder\UrlParameter $urlParameters;
protected int $parameterIndex = 0;
protected array $queryStack = [];
protected array $queryParamStack = [];
protected ? string $limitQueryField = "limit";
protected ? string $offsetQueryField = "offset";
protected ? string $pageQueryField = "page";
public function values(array $dataset) : self
{
@@ -27,79 +29,73 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
return $this;
}
public function where(/* stringable*/ $field, $value, string $operator = Ulmus\Query\Where::OPERATOR_EQUAL, string $condition = Ulmus\Query\Where::CONDITION_AND, bool $not = false) : self
public function filter(\Stringable|string $field, mixed $value,) : self
{
# Empty IN case
if ( [] === $value ) {
return $this;
if ( empty($this->filters) && ! $this->getFragment(RequestBuilder\Filter::class) ) {
$this->filters = new RequestBuilder\Filter();
$this->push($this->filters);
}
if ( $this->where ?? false ) {
$where = $this->where;
}
elseif ( null === ( $where = $this->getFragment(RequestBuilder\Filter::class) ) ) {
$this->where = $where = new RequestBuilder\Filter($this);
$this->push($where);
}
$this->whereConditionOperator = $operator;
$where->add($field, $value, $operator, $condition, $not);
$this->filters->add($field, $value);
return $this;
}
public function notWhere($field, $value, string $operator = Ulmus\Query\Where::CONDITION_AND) : self
public function where(\Stringable|string $field, mixed $value,) : self
{
return $this->where($field, $value, $operator, true);
return $this->filter($field, $value);
}
public function bindUrl(\Stringable|string $field, mixed $value,) : self
{
if ( empty($this->urlParameters) && ! $this->getFragment(RequestBuilder\UrlParameter::class) ) {
$this->urlParameters = new RequestBuilder\UrlParameter();
$this->push($this->urlParameters);
}
$this->urlParameters->add($field, $value);
return $this;
}
public function limit(int $value) : self
{
/*if ( null === $limit = $this->getFragment(Ulmus\Query\Limit::class) ) {
$limit = new Query\Limit();
$this->push($limit);
}
$limit->set($value);
*/
return $this;
return is_null($this->limitQueryField) ? $this : $this->filter($this->limitQueryField, $value);
}
public function offset(int $value) : self
{
if ( null === $offset = $this->getFragment(Ulmus\Query\Offset::class) ) {
$offset = new Ulmus\Query\Offset();
$this->push($offset);
return is_null($this->offsetQueryField) ? $this : $this->filter($this->offsetQueryField, $value);
}
$offset->set($value);
return $this;
public function page(int $value) : self
{
return is_null($this->pageQueryField) ? $this : $this->filter($this->pageQueryField, $value);
}
public function orderBy(string $field, ? string $direction = null) : self
{
return $this;
return $this->where($field, $direction);
}
public function push(Ulmus\Query\Fragment $queryFragment) : self
public function push(QueryFragmentInterface $queryFragment) : self
{
$this->queryStack[] = $queryFragment;
$this->queryParamStack[] = $queryFragment;
return $this;
}
public function pull(Ulmus\Query\Fragment $queryFragment) : self
public function pull(QueryFragmentInterface $queryFragment) : self
{
return array_shift($this->queryStack);
return array_shift($this->queryParamStack);
}
public function render(bool $skipToken = false) : array
{
$stack = [];
foreach($this->queryStack as $fragment) {
foreach($this->queryParamStack as $fragment) {
$stack = array_merge($stack, (array) $fragment->render($skipToken));
}
@@ -108,16 +104,12 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
public function reset() : void
{
$this->parameters = $this->values = $this->queryStack = [];
$this->whereConditionOperator = Ulmus\Query\Where::CONDITION_AND;
$this->parameterIndex = 0;
unset($this->where);
unset($this->filters);
}
public function getFragment(string $class, int $index = 0) : ? Ulmus\Query\Fragment
public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface
{
foreach($this->queryStack as $item) {
foreach($this->queryParamStack as $item) {
if ( get_class($item) === $class ) {
if ( $index-- === 0 ) {
return $item;
@@ -128,11 +120,11 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
return null;
}
public function removeFragment(\Ulmus\Query\Fragment|array|\Stringable|string $fragment) : void
public function removeFragment(QueryFragmentInterface|array|\Stringable|string $fragment) : void
{
foreach($this->queryStack as $key => $item) {
foreach($this->queryParamStack as $key => $item) {
if ( $item === $fragment ) {
unset($this->queryStack[$key]);
unset($this->queryParamStack[$key]);
}
}
}
@@ -142,23 +134,8 @@ class RequestBuilder implements Ulmus\Query\QueryBuilderInterface
return $this->render();
}
public function addParameter($value, string $key = null) : string
public function hashSerializedQuery(): string
{
if ( $this->parent ?? false ) {
return $this->parent->addParameter($value, $key);
}
if ( $key === null ) {
$key = ":p" . $this->parameterIndex++;
}
$this->parameters[$key] = $value;
return $key;
}
public function addValues(array $values) : void
{
$this->values = $values;
return "UNSUPPORTED";
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Ulmus\Api\RequestBuilder;
class Filter extends Fragment
{
public const KEY = "filters";
public array $conditionList;
public function add($field, mixed $value) : self
{
$this->conditionList[(string) $field] = $value;
return $this;
}
public function render() : array
{
return [
static::KEY => $this->conditionList
];
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Ulmus\Api\RequestBuilder;
use Ulmus\Query\QueryFragmentInterface;
abstract class Fragment implements QueryFragmentInterface {
protected int $encodingType = \PHP_QUERY_RFC3986;
protected ? string $querySeparator = null;
public abstract function render() : array;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Ulmus\Api\RequestBuilder;
class Pagination extends Fragment {
public array $conditionList;
public function add($field, mixed $value) : self
{
$this->conditionList[$field] = $value;
return $this;
}
public function render(bool $skipToken = false) : string
{
return http_build_query($this->conditionList);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Ulmus\Api\RequestBuilder;
class UrlParameter extends Fragment
{
public const KEY = "bindings";
public array $conditionList;
public function add($field, mixed $value) : self
{
$this->conditionList[$field] = $value;
return $this;
}
public function render() : array
{
return [
static::KEY => $this->conditionList
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Ulmus\Api\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Ulmus\Api\Common\XmlMessageTrait;
use Ulmus\Api\Stream\XmlStream;
use Ulmus\Api\Common\StreamOutputInterface;
class XmlResponse extends Response implements StreamOutputInterface
{
use XmlMessageTrait;
public function __construct(null|StreamInterface $stream = null, int $statusCode = 200, array $headers = [])
{
parent::__construct($stream, $statusCode, $headers + [ 'Content-Type' => "application/xml" ]);
}
public static function fromResponse(ResponseInterface $response) : static
{
return new static($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
public static function createFrom(mixed $content) : ResponseInterface
{
return new static(XmlStream::fromContent($content));
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Ulmus\Api\SearchRequest;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\{Api\SearchRequest\Attribute\BindParameter,
Repository,
SearchRequest\SearchMethodEnum,
SearchRequest\SearchRequest,
SearchRequest\SearchRequestFromRequestTrait,
SearchRequest\SearchRequestInterface,
SearchRequest\SearchRequestPaginationTrait};
class ApiSearchRequest extends SearchRequest implements SearchRequestInterface
{
use /*SearchRequestPaginationTrait,*/ SearchRequestFromRequestTrait {
parseAttributeMethod as parseAttributeMethodParent;
}
protected array $bindings = [];
public function bindings(): iterable
{
return array_filter($this->bindings + [
], fn($i) => !is_null($i)) + [];
}
protected function parseAttributeMethod(object $attribute, string|\Stringable $field, string $propertyName) : void
{
switch ($attribute->method) {
case SearchMethodEnum::Where:
case SearchMethodEnum::Manual:
if ($attribute instanceof BindParameter) {
if (isset($this->$propertyName)) {
$this->bindings[(string)$field] = $this->$propertyName;
}
return;
}
default:
$this->parseAttributeMethodParent($attribute, $field, $propertyName);
break;
}
}
}
@@ -0,0 +1,21 @@
<?php
namespace Ulmus\Api\SearchRequest\Attribute;
use Ulmus\SearchRequest\{Attribute\PropertyValueSource,
Attribute\SearchManual,
SearchMethodEnum,
Attribute\SearchParameter};
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class BindParameter extends SearchManual
{
public function __construct(
public null|string|array $parameter = null,
public null|string|\Stringable|array $field = null,
public bool $toggle = false,
public SearchMethodEnum $method = SearchMethodEnum::Manual,
public string $description = "",
public PropertyValueSource|array $source = PropertyValueSource::RequestAttribute,
) {}
}
+2 -2
View File
@@ -107,7 +107,7 @@ class Stream implements StreamInterface
return $this->hasMode('acwx+');
}
public function write($string) : false|int
public function write($string) : int
{
if (! $this->isWritable()) {
throw new \UnexpectedValueException("An error occured while trying to seek an unseekable stream.");
@@ -166,7 +166,7 @@ class Stream implements StreamInterface
return $result;
}
public function getMetadata(string $key = null) : mixed
public function getMetadata(?string $key = null) : mixed
{
$metadata = stream_get_meta_data($this->stream);
+49
View File
@@ -0,0 +1,49 @@
<?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;
# @TODO
class XmlStream extends Stream
{
public function __construct()
{
}
public static function fromContent(mixed $content) : StreamInterface
{
#if ( false === $json = json_encode($content) ) {
# throw new \InvalidArgumentException(sprintf('Json encoding failed with message `%s`.', json_last_error_msg()));
# }
# @TODO !
#return static::fromJsonEncoded($json);
return static::fromXmlEncoded($content);
}
public static function fromXmlEncoded(string $xml) : StreamInterface
{
$obj = static::fromTemp($xml);
$obj->rewind();
return $obj;
}
public function decode(): mixed
{
$content = $this->getContents();
if ($content === "") {
return null;
}
return json_decode(json_encode((array) \simplexml_load_string($content)), true);
}
}
+71 -10
View File
@@ -2,39 +2,52 @@
namespace Ulmus\Api\Transport;
use League\OAuth2\Client\Provider\GenericProvider;
use Ulmus\Api\Common\AuthenticationEnum;
class CurlAuthorization
{
public array $headers = [];
public function __construct(
#[\SensitiveParameter]
public array $configuration,
#[\SensitiveParameter]
public array $headers = [],
public array $options = [],
) { }
public array $options = [];
public function fromAuthenticationEnum(AuthenticationEnum $auth, string|false $username, #[\SensitiveParameter] string|false $password, #[\SensitiveParameter] string|false $token,) : static
public function fromAuthenticationEnum(AuthenticationEnum $auth,) : static
{
switch($auth) {
case AuthenticationEnum::Custom:
$this->customHeader($this->configuration['header'], $this->configuration['secret']);
break;
case AuthenticationEnum::Bearer:
$this->bearer($token);
$this->bearer($this->configuration['token']);
break;
case AuthenticationEnum::Key:
$this->key($token);
$this->key($this->configuration['token']);
break;
case AuthenticationEnum::Token:
$this->token($token);
$this->token($this->configuration['token']);
break;
case AuthenticationEnum::Basic:
$this->basic($username, $password);
$this->basic($this->configuration['username'], $this->configuration['password']);
break;
case AuthenticationEnum::Ntlm:
$this->ntlm($username, $password);
$this->ntlm($this->configuration['username'], $this->configuration['password']);
break;
case AuthenticationEnum::Negotiate:
$this->negotiate($username, $password);
$this->negotiate($this->configuration['username'], $this->configuration['password']);
break;
case AuthenticationEnum::OAuth2:
$this->oauth2($this->configuration['grant'], $this->configuration['oauth_token_url'], $this->configuration['redirect_uri'], $this->configuration['client_id'], $this->configuration['client_secret'], $this->configuration['scope']);
break;
}
@@ -43,6 +56,7 @@ class CurlAuthorization
public function bearer(#[\SensitiveParameter] string $token) : void
{
$this->options[CURLOPT_HTTPAUTH] = CURLAUTH_BEARER;
$this->options[CURLOPT_XOAUTH2_BEARER] = $token;
}
@@ -86,6 +100,53 @@ class CurlAuthorization
$this->userpass(CURLAUTH_NEGOTIATE, $username, $password);
}
public function customHeader(string $header, #[\SensitiveParameter] string $token) : void
{
$this->headers[$header] = $token;
}
public function oauth2(Oauth2GrantTypeEnum $grant, string $oauthTokenUrl, string $redirectUri, string $clientId, string $clientSecret, string $scope) : void
{
$unid = sprintf("oauth_token_%s", md5(serialize($this->configuration)));
$token = apcu_fetch($unid, $exists);
if ( ! $exists ) {
$provider = new GenericProvider([
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'redirectUri' => $redirectUri,
'urlAccessToken' => $oauthTokenUrl,
'urlAuthorize' => 'https://service.example.com/authorize',
'urlResourceOwnerDetails' => 'https://service.example.com/resource',
'scopes' => $scope,
]);
try {
$accessToken = $provider->getAccessToken($grant->value);
$token = json_encode($accessToken);
apcu_store($unid, $token, $accessToken->getExpires() - time() - 10);
}
catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
throw $e;
}
}
try {
$decodedToken = json_decode($token, true, 512, JSON_THROW_ON_ERROR);
}
catch(\Exception $ex) {
throw new \InvalidArgumentException("Given OAuth2 token is not a parsable JSON stream.");
}
if ( empty($decodedToken) ) {
throw new \InvalidArgumentException("Given OAuth Token is empty");
}
$this->bearer($decodedToken['access_token']);
}
protected function userpass(int $authType, string $username, #[\SensitiveParameter] $password) : void
{
$this->options[CURLOPT_HTTPAUTH] = $authType;
+22 -8
View File
@@ -6,7 +6,7 @@ use Ulmus\Api\Common\{ ContentTypeEnum, MethodEnum, HttpHeaderEnum };
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Psr\Http\Message\{RequestInterface, ResponseInterface, StreamInterface};
use Ulmus\Api\Response\{ Response, JsonResponse };
use Ulmus\Api\Response\{Response, JsonResponse, XmlResponse};
abstract class CurlTransport {
@@ -27,7 +27,7 @@ abstract class CurlTransport {
public ContentTypeEnum $contentType = ContentTypeEnum::Json,
# Matching Guzzle's great user-agent syntax
public string $userAgent = "ulmus-api/1.0 curl/{curl} php/{php}",
public bool $debug = false,
public bool $debug = true,
) {
$this->userAgent = str_replace([ '{curl}', '{php}', ], [ curl_version()['version'] ?? "???", phpversion() ], $this->userAgent);
}
@@ -37,9 +37,9 @@ abstract class CurlTransport {
return $this->request(MethodEnum::Post, $url, $data, $headers, $curlOptions);
}
public function get(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
public function get(string $url, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Get, $url, $data, $headers, $curlOptions);
return $this->request(MethodEnum::Get, $url, null, $headers, $curlOptions);
}
public function delete(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
@@ -77,9 +77,10 @@ abstract class CurlTransport {
$this->applyMethod($method, $data, $headers, $options);
$headers += $this->authorization->headers;
if ($this->authorization) {
$headers += HttpHeaderEnum::normalizeHeaderArray($this->authorization->headers);
$options += $this->authorization->options;
}
$options += [
CURLOPT_URL => $url,
@@ -129,13 +130,26 @@ abstract class CurlTransport {
$response = $response->withBody( Stream::fromMemory($result) );
if ( $response->hasHeader('Content-Type') && false !== strpos($response->getHeader('Content-Type')[0], 'json') ) {
$response = JsonResponse::fromResponse($response);
if ($response->hasHeader('Content-Type') ) {
list($mime) = explode(';', $response->getHeader('Content-Type')[0]);
return match(ContentTypeEnum::tryFrom($mime)) {
ContentTypeEnum::Json => JsonResponse::fromResponse($response),
ContentTypeEnum::RssXml, ContentTypeEnum::Xml => XmlResponse::fromResponse($response),
default => $response,
};
}
return $response;
}
public function getDebugErrorLines() : array
{
$this->stderr->rewind();
return explode(PHP_EOL, $this->stderr->getContents());
}
protected function applyMethod(MethodEnum $method, mixed &$data, array &$headers, array &$options) : void
{
switch ($method) {
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Ulmus\Api\Transport;
enum Oauth2GrantTypeEnum : string
{
case AuthorizationCode = "authorization_code";
case PKCE = "pkce";
case ClientCredentials = "client_credentials";
case DeviceCode = "device_code";
case RefreshToken = "refresh_token";
}