From 799e66d4789a48b23b3ec2e2ab3a7b0405021092 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 1 Mar 2021 09:49:11 -0500 Subject: [PATCH] Added support for PSR-3 (#272) * Added support for PSR-3 Co-authored-by: Greg Holmes --- README.md | 17 +++++++-- composer.json | 3 +- src/Client.php | 59 ++++++++++++++++++++++++++++- src/Client/Factory/MapFactory.php | 9 +++++ src/Logger/LoggerAwareInterface.php | 21 ++++++++++ src/Logger/LoggerTrait.php | 35 +++++++++++++++++ test/ClientTest.php | 16 ++++++++ test/Logger/LoggerTraitTest.php | 46 ++++++++++++++++++++++ 8 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/Logger/LoggerAwareInterface.php create mode 100644 src/Logger/LoggerTrait.php create mode 100644 test/Logger/LoggerTraitTest.php diff --git a/README.md b/README.md index 7a5da17b..c7eb6a01 100644 --- a/README.md +++ b/README.md @@ -814,8 +814,20 @@ If you have a conflicting package installation that cannot co-exist with our rec See the [Packagist page for client-implementation](https://packagist.org/providers/php-http/client-implementation) for options. -Contributing ------------- +### Enabling Request/Response Logging + +Our client library has support for logging the request and response for debugging via PSR-3 compatible logging mechanisms. If the `debug` option is passed into the client and a PSR-3 compatible logger is set in our client's service factory, we will use the logger for debugging purposes. + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic('abcd1234', 's3cr3tk3y'), ['debug' => true]); +$logger = new \Monolog\Logger('test'); +$logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/log.txt', \Monolog\Logger::DEBUG)); +$client->getFactory()->set(\PSR\Log\LoggerInterface::class, $logger); +``` + +**ENABLING DEBUGING LOGGING HAS THE POTENTIAL FOR LOGGING SENSITIVE INFORMATION, DO NOT ENABLE IN PRODUCTION** + +## Contributing This library is actively developed, and we love to hear from you! Please feel free to [create an issue][issues] or [open a pull request][pulls] with your questions, comments, suggestions and feedback. @@ -828,4 +840,3 @@ This library is actively developed, and we love to hear from you! Please feel fr [spec]: https://github.com/Nexmo/client-library-specification [issues]: https://github.com/Vonage/vonage-php-core/issues [pulls]: https://github.com/Vonage/vonage-php-core/pulls - diff --git a/composer.json b/composer.json index 17ca7134..dababe80 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "composer/package-versions-deprecated": "^1.11", "psr/container": "^1.0", "psr/http-client-implementation": "^1.0", - "vonage/nexmo-bridge": "^0.1.0" + "vonage/nexmo-bridge": "^0.1.0", + "psr/log": "^1.1" }, "require-dev": { "guzzlehttp/guzzle": ">=6", diff --git a/src/Client.php b/src/Client.php index 03bfb6c7..b8b2a87c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -22,6 +22,8 @@ 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; @@ -41,6 +43,7 @@ use Vonage\Conversion\ClientFactory as ConversionClientFactory; use Vonage\Entity\EntityInterface; use Vonage\Insights\ClientFactory as InsightsClientFactory; +use Vonage\Logger\LoggerAwareInterface; use Vonage\Message\Client as MessageClient; use Vonage\Numbers\ClientFactory as NumbersClientFactory; use Vonage\Redact\ClientFactory as RedactClientFactory; @@ -68,6 +71,7 @@ use function str_replace; use function strpos; use function unserialize; +use Vonage\Logger\LoggerTrait; /** * Vonage API Client, allows access to the API from PHP. @@ -86,8 +90,10 @@ * @property string restUrl * @property string apiUrl */ -class Client +class Client implements LoggerAwareInterface { + use LoggerTrait; + public const VERSION = '2.5.0'; public const BASE_API = 'https://api.nexmo.com'; public const BASE_REST = 'https://rest.nexmo.com'; @@ -106,15 +112,25 @@ class Client */ protected $client; + /** + * @var bool + */ + protected $debug = false; + /** * @var ContainerInterface */ protected $factory; + /** + * @var LoggerInterface + */ + protected $logger; + /** * @var array */ - protected $options = ['show_deprecations' => false]; + protected $options = ['show_deprecations' => false, 'debug' => false]; /** * Create a new API client using the provided credentials. @@ -176,6 +192,10 @@ public function __construct(CredentialsInterface $credentials, $options = [], ?C $this->apiUrl = $options['base_api_url']; } + if (isset($options['debug'])) { + $this->debug = $options['debug']; + } + $this->setFactory( new MapFactory( [ @@ -532,6 +552,32 @@ public function send(RequestInterface $request): ResponseInterface /** @noinspection PhpUnnecessaryLocalVariableInspection */ $response = $this->client->sendRequest($request); + if ($this->debug) { + $id = uniqid(); + $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; } @@ -637,4 +683,13 @@ protected function getVersion(): string { return Versions::getVersion('vonage/client-core'); } + + public function getLogger(): ?LoggerInterface + { + if (!$this->logger && $this->getFactory()->has(LoggerInterface::class)) { + $this->setLogger($this->getFactory()->get(LoggerInterface::class)); + } + + return $this->logger; + } } diff --git a/src/Client/Factory/MapFactory.php b/src/Client/Factory/MapFactory.php index b9fa9928..2b53bec6 100644 --- a/src/Client/Factory/MapFactory.php +++ b/src/Client/Factory/MapFactory.php @@ -12,8 +12,10 @@ namespace Vonage\Client\Factory; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use RuntimeException; use Vonage\Client; +use Vonage\Logger\LoggerAwareInterface; use function is_callable; use function sprintf; @@ -120,11 +122,18 @@ public function make($key) $instance->setClient($this->client); } + if ($instance instanceof LoggerAwareInterface && $this->has(LoggerInterface::class)) { + $instance->setLogger($this->get(LoggerInterface::class)); + } + return $instance; } public function set($key, $value): void { $this->map[$key] = $value; + if (!is_callable($value)) { + $this->cache[$key] = $value; + } } } diff --git a/src/Logger/LoggerAwareInterface.php b/src/Logger/LoggerAwareInterface.php new file mode 100644 index 00000000..9c666cc8 --- /dev/null +++ b/src/Logger/LoggerAwareInterface.php @@ -0,0 +1,21 @@ + $context Additional information for context + */ + public function log($level, string $message, array $context = []): void; + + /** + * @return self + */ + public function setLogger(LoggerInterface $logger); +} diff --git a/src/Logger/LoggerTrait.php b/src/Logger/LoggerTrait.php new file mode 100644 index 00000000..b58443ad --- /dev/null +++ b/src/Logger/LoggerTrait.php @@ -0,0 +1,35 @@ +logger; + } + + /** + * @param string|int $level Level of message that we are logging + * @param array $context Additional information for context + */ + public function log($level, string $message, array $context = []): void + { + $logger = $this->getLogger(); + if ($logger) { + $logger->log($level, $message, $context); + } + } + + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } +} diff --git a/test/ClientTest.php b/test/ClientTest.php index f8aeda95..e8a16f1e 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -19,6 +19,7 @@ use Laminas\Diactoros\Response; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientExceptionInterface; +use Psr\Log\LoggerInterface; use RuntimeException; use Vonage\Client; use Vonage\Client\Credentials\Basic; @@ -664,6 +665,21 @@ public function testGenericDeleteMethod($url, $params): void $this->assertRequestBodyIsEmpty($request); } + public function testLoggerIsNullWhenNotSet(): void + { + $client = new Client($this->basic_credentials, [], $this->http); + $this->assertNull($client->getLogger()); + } + + public function testCanGetLoggerWhenOneIsSet(): void + { + $client = new Client($this->basic_credentials, [], $this->http); + $logger = $this->prophesize(LoggerInterface::class); + $client->getFactory()->set(LoggerInterface::class, $logger->reveal()); + + $this->assertNotNull($client->getLogger()); + } + public function genericDeleteProvider(): array { $baseUrl = 'https://rest.nexmo.com'; diff --git a/test/Logger/LoggerTraitTest.php b/test/Logger/LoggerTraitTest.php new file mode 100644 index 00000000..0e68307d --- /dev/null +++ b/test/Logger/LoggerTraitTest.php @@ -0,0 +1,46 @@ +getMockForTrait(LoggerTrait::class); + $logger = $this->prophesize(LoggerInterface::class)->reveal(); + $trait->setLogger($logger); + + $this->assertSame($logger, $trait->getLogger()); + } + + public function testNoLoggerReturnsNull() + { + /** @var LoggerTrait $trait */ + $trait = $this->getMockForTrait(LoggerTrait::class); + + $this->assertNull($trait->getLogger()); + } + + public function testCanLogMessageWithLogger() + { + /** @var LoggerTrait $trait */ + $trait = $this->getMockForTrait(LoggerTrait::class); + $logger = $this->prophesize(LoggerInterface::class)->reveal(); + $trait->setLogger($logger); + + $this->assertNull($trait->log('debug', 'This is a message')); + } + + public function testLoggingAcceptsMessageWithLogger() + { + /** @var LoggerTrait $trait */ + $trait = $this->getMockForTrait(LoggerTrait::class); + + $this->assertNull($trait->log('debug', 'This is a message')); + } +} \ No newline at end of file