From 88594b4525e7265727c6da0b2bbe2cdfb6524267 Mon Sep 17 00:00:00 2001 From: Jim Seconde Date: Fri, 6 Dec 2024 13:06:59 +0000 Subject: [PATCH] remove all client http methods, implement some methods into API resource, prepare for logger and options value objects --- src/Client.php | 323 +------------------------------------ src/Client/APIResource.php | 113 ++++++++++++- 2 files changed, 112 insertions(+), 324 deletions(-) diff --git a/src/Client.php b/src/Client.php index aa866fb4..99f858e1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,18 +5,9 @@ namespace Vonage; use Composer\InstalledVersions; -use Http\Client\HttpClient; -use InvalidArgumentException; -use Laminas\Diactoros\Request; -use Laminas\Diactoros\Uri; -use Lcobucci\JWT\Token; use Psr\Container\ContainerInterface; -use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; use RuntimeException; use Vonage\Account\ClientFactory; use Vonage\Application\ClientFactory as ApplicationClientFactory; @@ -25,20 +16,11 @@ use Vonage\Client\Credentials\Container; use Vonage\Client\Credentials\CredentialsInterface; use Vonage\Client\Credentials\Gnp; -use Vonage\Client\Credentials\Handler\BasicHandler; -use Vonage\Client\Credentials\Handler\SignatureBodyFormHandler; -use Vonage\Client\Credentials\Handler\SignatureBodyHandler; -use Vonage\Client\Credentials\Handler\SignatureQueryHandler; -use Vonage\Client\Credentials\Handler\TokenBodyFormHandler; -use Vonage\Client\Credentials\Handler\TokenBodyHandler; -use Vonage\Client\Credentials\Handler\TokenQueryHandler; use Vonage\Client\Credentials\Keypair; use Vonage\Client\Credentials\SignatureSecret; -use Vonage\Client\Exception\Exception as ClientException; use Vonage\Client\Factory\FactoryInterface; use Vonage\Client\Factory\MapFactory; use Vonage\Conversion\ClientFactory as ConversionClientFactory; -use Vonage\Entity\EntityInterface; use Vonage\Insights\ClientFactory as InsightsClientFactory; use Vonage\Meetings\ClientFactory as MeetingsClientFactory; use Vonage\Numbers\ClientFactory as NumbersClientFactory; @@ -53,21 +35,14 @@ use Vonage\Verify\ClientFactory as VerifyClientFactory; use Vonage\Verify2\ClientFactory as Verify2ClientFactory; use Vonage\Conversation\ClientFactory as ConversationClientFactory; -use Vonage\Verify\Verification; use Vonage\Voice\ClientFactory as VoiceClientFactory; use Vonage\Logger\{LoggerAwareInterface, LoggerTrait}; use function array_key_exists; use function array_merge; use function call_user_func_array; -use function http_build_query; -use function implode; use function is_null; -use function json_encode; -use function method_exists; use function set_error_handler; -use function str_replace; -use function strpos; /** * Vonage API Client, allows access to the API from PHP. @@ -99,9 +74,6 @@ class Client implements LoggerAwareInterface { use LoggerTrait; - public const BASE_API = 'https://api.nexmo.com'; - public const BASE_REST = 'https://rest.nexmo.com'; - protected CredentialsInterface $credentials; protected ClientInterface $client; @@ -117,16 +89,12 @@ class Client implements LoggerAwareInterface protected array $options = ['show_deprecations' => false, 'debug' => false]; - public string $apiUrl; - - public string $restUrl; - /** * Create a new API client using the provided credentials. */ public function __construct( CredentialsInterface $credentials, - $options = [], + ?VonageConfig $options = null, ?ClientInterface $client = null ) { if (is_null($client)) { @@ -136,9 +104,6 @@ public function __construct( $guzzleVersion = (float) $guzzleVersion; if ($guzzleVersion >= 6.0 && $guzzleVersion < 7) { - /** @noinspection CallableParameterUseCaseInTypeContextInspection */ - /** @noinspection PhpUndefinedNamespaceInspection */ - /** @noinspection PhpUndefinedClassInspection */ $client = new \Http\Adapter\Guzzle6\Client(); } @@ -147,8 +112,6 @@ public function __construct( } } - $this->setHttpClient($client); - if ( !($credentials instanceof Container) && !($credentials instanceof Basic) && @@ -168,25 +131,6 @@ public function __construct( $this->validateAppOptions($options['app']); } - // Set the default URLs. Keep the constants for - // backwards compatibility - $this->apiUrl = static::BASE_API; - $this->restUrl = static::BASE_REST; - - // If they've provided alternative URLs, use that instead - // of the defaults - if (isset($options['base_rest_url'])) { - $this->restUrl = $options['base_rest_url']; - } - - if (isset($options['base_api_url'])) { - $this->apiUrl = $options['base_api_url']; - } - - if (isset($options['debug'])) { - $this->debug = $options['debug']; - } - $services = [ // Registered Services by name 'account' => ClientFactory::class, @@ -283,217 +227,6 @@ public function getFactory(): ContainerInterface return $this->factory; } - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public static function signRequest(RequestInterface $request, SignatureSecret $credentials): RequestInterface - { - $handler = match ($request->getHeaderLine('content-type')) { - 'application/json' => new SignatureBodyHandler(), - 'application/x-www-form-urlencoded' => new SignatureBodyFormHandler(), - default => new SignatureQueryHandler(), - }; - - return $handler($request, $credentials); - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public static function authRequest(RequestInterface $request, Basic $credentials): RequestInterface - { - switch ($request->getHeaderLine('content-type')) { - case 'application/json': - if (static::requiresBasicAuth($request)) { - $handler = new BasicHandler(); - } elseif (static::requiresAuthInUrlNotBody($request)) { - $handler = new TokenQueryHandler(); - } else { - $handler = new TokenBodyHandler(); - } - break; - case 'application/x-www-form-urlencoded': - $handler = new TokenBodyFormHandler(); - break; - default: - if (static::requiresBasicAuth($request)) { - $handler = new BasicHandler(); - } else { - $handler = new TokenQueryHandler(); - } - break; - } - - return $handler($request, $credentials); - } - - /** - * @throws ClientException - */ - public function generateJwt($claims = []): Token - { - if (method_exists($this->credentials, "generateJwt")) { - return $this->credentials->generateJwt($claims); - } - - throw new ClientException($this->credentials::class . ' does not support JWT generation'); - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public function get(string $url, array $params = []): ResponseInterface - { - $queryString = '?' . http_build_query($params); - $url .= $queryString; - - $request = new Request($url, 'GET'); - - return $this->send($request); - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public function post(string $url, array $params): ResponseInterface - { - $request = new Request( - $url, - 'POST', - 'php://temp', - ['content-type' => 'application/json'] - ); - - $request->getBody()->write(json_encode($params)); - - return $this->send($request); - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public function postUrlEncoded(string $url, array $params): ResponseInterface - { - $request = new Request( - $url, - 'POST', - 'php://temp', - ['content-type' => 'application/x-www-form-urlencoded'] - ); - - $request->getBody()->write(http_build_query($params)); - - return $this->send($request); - } - - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public function put(string $url, array $params): ResponseInterface - { - $request = new Request( - $url, - 'PUT', - 'php://temp', - ['content-type' => 'application/json'] - ); - - $request->getBody()->write(json_encode($params)); - - return $this->send($request); - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - public function delete(string $url): ResponseInterface - { - $request = new Request( - $url, - 'DELETE' - ); - - return $this->send($request); - } - - /** - * Wraps the HTTP Client, creates a new PSR-7 request adding authentication, signatures, etc. - * - * @throws ClientExceptionInterface - */ - public function send(RequestInterface $request): ResponseInterface - { - // Allow any part of the URI to be replaced with a simple search - if (isset($this->options['url'])) { - foreach ($this->options['url'] as $search => $replace) { - $uri = (string)$request->getUri(); - $new = str_replace($search, $replace, $uri); - - if ($uri !== $new) { - $request = $request->withUri(new Uri($new)); - } - } - } - - // The user agent must be in the following format: - // LIBRARY-NAME/LIBRARY-VERSION LANGUAGE-NAME/LANGUAGE-VERSION [APP-NAME/APP-VERSION] - // See https://github.com/Vonage/client-library-specification/blob/master/SPECIFICATION.md#reporting - $userAgent = []; - - // Library name - $userAgent[] = 'vonage-php/' . $this->getVersion(); - - // Language name - $userAgent[] = 'php/' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - - // If we have an app set, add that to the UA - if (isset($this->options['app'])) { - $app = $this->options['app']; - $userAgent[] = $app['name'] . '/' . $app['version']; - } - - // Set the header. Build by joining all the parts we have with a space - $request = $request->withHeader('User-Agent', implode(' ', $userAgent)); - /** @noinspection PhpUnnecessaryLocalVariableInspection */ - $response = $this->client->sendRequest($request); - - if ($this->debug) { - $id = uniqid('', true); - $request->getBody()->rewind(); - $response->getBody()->rewind(); - $this->log( - LogLevel::DEBUG, - 'Request ' . $id, - [ - 'url' => $request->getUri()->__toString(), - 'headers' => $request->getHeaders(), - 'body' => explode("\n", $request->getBody()->__toString()) - ] - ); - $this->log( - LogLevel::DEBUG, - 'Response ' . $id, - [ - 'headers ' => $response->getHeaders(), - 'body' => explode("\n", $response->getBody()->__toString()) - ] - ); - - $request->getBody()->rewind(); - $response->getBody()->rewind(); - } - - return $response; - } - protected function validateAppOptions($app): void { $disallowedCharacters = ['/', ' ', "\t", "\n"]; @@ -538,18 +271,6 @@ public function __get($name) return $this->factory->get($name); } - /** - * @deprecated Use the Verify Client, this shouldn't be here and will be removed. - */ - public function serialize(EntityInterface $entity): string - { - if ($entity instanceof Verification) { - return $this->verify()->serialize($entity); - } - - throw new RuntimeException('unknown class `' . $entity::class . '``'); - } - protected function getVersion(): string { return InstalledVersions::getVersion('vonage/client-core'); @@ -568,46 +289,4 @@ public function getCredentials(): CredentialsInterface { return $this->credentials; } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - protected static function requiresBasicAuth(RequestInterface $request): bool - { - $path = $request->getUri()->getPath(); - $isSecretManagementEndpoint = str_starts_with($path, '/accounts') && str_contains($path, '/secrets'); - $isApplicationV2 = str_starts_with($path, '/v2/applications'); - - return $isSecretManagementEndpoint || $isApplicationV2; - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - protected static function requiresAuthInUrlNotBody(RequestInterface $request): bool - { - $path = $request->getUri()->getPath(); - - $isRedact = str_starts_with($path, '/v1/redact'); - $isMessages = str_starts_with($path, '/v1/messages'); - - return $isRedact || $isMessages; - } - - /** - * @deprecated Use a configured APIResource with a HandlerInterface - * Request business logic is being removed from the User Client Layer. - */ - protected function needsKeypairAuthentication(RequestInterface $request): bool - { - $path = $request->getUri()->getPath(); - $isCallEndpoint = str_starts_with($path, '/v1/calls'); - $isRecordingUrl = str_starts_with($path, '/v1/files'); - $isStitchEndpoint = str_starts_with($path, '/beta/conversation'); - $isUserEndpoint = str_starts_with($path, '/beta/users'); - - return $isCallEndpoint || $isRecordingUrl || $isStitchEndpoint || $isUserEndpoint; - } } diff --git a/src/Client/APIResource.php b/src/Client/APIResource.php index dda10b43..06949e41 100644 --- a/src/Client/APIResource.php +++ b/src/Client/APIResource.php @@ -4,7 +4,12 @@ namespace Vonage\Client; +use Composer\InstalledVersions; +use GuzzleHttp\Client as GuzzleClient; +use Http\Adapter\Guzzle6\Client; use Laminas\Diactoros\Request; +use Laminas\Diactoros\Uri; +use Psr\Http\Client\ClientInterface; use Psr\Log\LogLevel; use Vonage\Client\Credentials\Handler\BasicHandler; use Vonage\Entity\Filter\EmptyFilter; @@ -21,9 +26,8 @@ use function json_encode; use function http_build_query; -class APIResource implements ClientAwareInterface +class APIResource { - use ClientAwareTrait; use LoggerTrait; /** @@ -61,6 +65,42 @@ class APIResource implements ClientAwareInterface protected ?ResponseInterface $lastResponse = null; + protected ?ClientInterface $client = null; + protected bool $debug; + + public function __construct(?ClientInterface $client = null) + { + if (is_null($client)) { + // Since the user did not pass a client, try and make a client + // using the Guzzle 6 adapter or Guzzle 7 (depending on availability) + [$guzzleVersion] = explode('@', (string) InstalledVersions::getVersion('guzzlehttp/guzzle'), 1); + $guzzleVersion = (float) $guzzleVersion; + + if ($guzzleVersion >= 6.0 && $guzzleVersion < 7) { + /** @noinspection CallableParameterUseCaseInTypeContextInspection */ + /** @noinspection PhpUndefinedClassInspection */ + $client = new Client(); + } + + if ($guzzleVersion >= 7.0 && $guzzleVersion < 8.0) { + $client = new GuzzleClient(); + } + + $this->setHttpClient($client); + } + } + + public function getHttpClient(): ?ClientInterface + { + return $this->client; + } + + public function setHttpClient(?ClientInterface $client): APIResource + { + $this->client = $client; + return $this; + } + /** * Adds authentication to a request * @@ -89,6 +129,75 @@ public function addAuth(RequestInterface $request): RequestInterface return $this->getAuthHandlers()($request, $credentials); } + /** + * Wraps the HTTP Client, creates a new PSR-7 request adding authentication, signatures, etc. + * + * @throws ClientExceptionInterface + */ + public function send(RequestInterface $request): ResponseInterface + { + // Allow any part of the URI to be replaced with a simple search + if (isset($this->options['url'])) { + foreach ($this->options['url'] as $search => $replace) { + $uri = (string)$request->getUri(); + $new = str_replace($search, $replace, $uri); + + if ($uri !== $new) { + $request = $request->withUri(new Uri($new)); + } + } + } + + // The user agent must be in the following format: + // LIBRARY-NAME/LIBRARY-VERSION LANGUAGE-NAME/LANGUAGE-VERSION [APP-NAME/APP-VERSION] + // See https://github.com/Vonage/client-library-specification/blob/master/SPECIFICATION.md#reporting + $userAgent = []; + + // Library name + $userAgent[] = 'vonage-php/' . $this->getVersion(); + + // Language name + $userAgent[] = 'php/' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + + // If we have an app set, add that to the UA + if (isset($this->options['app'])) { + $app = $this->options['app']; + $userAgent[] = $app['name'] . '/' . $app['version']; + } + + // Set the header. Build by joining all the parts we have with a space + $request = $request->withHeader('User-Agent', implode(' ', $userAgent)); + $response = $this->client->sendRequest($request); + + if ($this->debug) { + $id = uniqid('', true); + $request->getBody()->rewind(); + $response->getBody()->rewind(); + $this->log( + LogLevel::DEBUG, + 'Request ' . $id, + [ + 'url' => $request->getUri()->__toString(), + 'headers' => $request->getHeaders(), + 'body' => explode("\n", $request->getBody()->__toString()) + ] + ); + $this->log( + LogLevel::DEBUG, + 'Response ' . $id, + [ + 'headers ' => $response->getHeaders(), + 'body' => explode("\n", $response->getBody()->__toString()) + ] + ); + + $request->getBody()->rewind(); + $response->getBody()->rewind(); + } + + return $response; + } + /** * @throws ClientExceptionInterface * @throws Exception\Exception