Skip to content

Commit

Permalink
add debugging capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
Naoray committed Jan 15, 2025
1 parent 23316fe commit 998f600
Show file tree
Hide file tree
Showing 53 changed files with 895 additions and 612 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ For detailed documentation about using this PHP client, see the following guides
- [Requests](docs/requests.md) - Overview and usage of request objects in the API client.
- [Responses](docs/responses.md) - Handling and understanding responses from the API.
- [Testing](docs/testing.md) - Guidelines for testing with the Mollie API client.
- [Debugging](docs/debugging.md) - How to debug API requests and responses safely.

These guides provide in-depth explanations and examples for advanced usage of the client.

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"guzzlehttp/guzzle": "^7.6",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/var-dumper": "^6.0"
"symfony/var-dumper": "^7.0"
},
"suggest": {
"mollie/oauth2-mollie-php": "Use OAuth to authenticate with the Mollie API. This is needed for some endpoints. Visit https://docs.mollie.com/ for more information."
Expand Down
86 changes: 86 additions & 0 deletions docs/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Debugging with Mollie API Client

## Overview

The Mollie API client provides powerful debugging capabilities to help you inspect and troubleshoot API requests and responses. The debugging functionality is implemented through middleware and automatically sanitizes sensitive data to prevent accidental exposure of credentials.

## Basic Usage

### Enable All Debugging

To enable both request and response debugging:

```php
$mollie = new \Mollie\Api\MollieApiClient();
$mollie->debug(); // Enables both request and response debugging
```

### Debug Specific Parts

You can choose to debug only requests or only responses:

```php
// Debug only requests
$mollie->debugRequest();

// Debug only responses
$mollie->debugResponse();
```

## Custom Debuggers

You can provide your own debugging functions to customize how debugging information is displayed:

```php
// Custom request debugger
$mollie->debugRequest(function($pendingRequest, $psrRequest) {
// Your custom debugging logic here
});

// Custom response debugger
$mollie->debugResponse(function($response, $psrResponse) {
// Your custom debugging logic here
});
```

## Security Features

### Automatic Sanitization

When debugging is enabled, the client automatically:
- Removes sensitive headers (Authorization, User-Agent, etc.)
- Sanitizes request data to prevent credential exposure
- Handles exceptions safely by removing sensitive data

### Die After Debug

For development purposes, you can halt execution after debugging output:

```php
$mollie->debug(die: true); // Will stop execution after debugging output
```

## Best Practices

1. **Development Only**: Never enable debugging in production environments
2. **Custom Debuggers**: When implementing custom debuggers, ensure they handle sensitive data appropriately
3. **Exception Handling**: Debug mode works with exceptions, helping you troubleshoot API errors safely

## Example Usage

```php
try {
$mollie = new \Mollie\Api\MollieApiClient();
$mollie->setApiKey("test_dHar4XY7LxsDOtmnkVtjNVWXLSlXsM");

// Enable debugging for development
$mollie->debug();

// Your API calls here
$payment = $mollie->payments->create([...]);

} catch (\Mollie\Api\Exceptions\ApiException $e) {
// Exception will include sanitized debug information
echo "API call failed: " . htmlspecialchars($e->getMessage());
}
```
30 changes: 1 addition & 29 deletions docs/http-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,38 +37,10 @@ class MyCustomHttpAdapter implements HttpAdapterContract {
}
```

### Debugging

When debugging mode is enabled, adapters remove sensitive information before they fire an exception.

Adapters that support debugging must implement the `SupportsDebuggingContract`. This contract defines methods `enableDebugging()` and `disableDebugging()` to control debugging behavior.

```php
use Mollie\Api\Contracts\SupportsDebuggingContract;

class MyCustomHttpAdapter implements HttpAdapterContract, SupportsDebuggingContract {
// Implementation of debugging methods
}
```

## Available Adapters

Out of the box, the Mollie API client provides several adapters:

- **GuzzleMollieHttpAdapter**: Wraps a Guzzle HTTP client for sending requests.
- **CurlMollieHttpAdapter**: Uses cURL for sending HTTP requests. This is the default if Guzzle is not available.

## Enabling Debugging

Debugging can be enabled through the `HandlesDebugging` trait. This trait allows you to toggle debugging on the HTTP client, which is useful for development and troubleshooting.

### How to Enable Debugging

1. **Enable Debugging**: Call `enableDebugging()` on the Mollie API client instance. This sets the debugging mode on the underlying HTTP adapter, if it supports debugging.

2. **Disable Debugging**: Call `disableDebugging()` to turn off debugging.

```php
$mollieClient->enableDebugging();
$mollieClient->disableDebugging();
```
- **PSR18MollieHttpAdapter**: psr-18 compatible adapter
14 changes: 7 additions & 7 deletions src/CompatibilityChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Mollie\Api;

use Mollie\Api\Exceptions\IncompatiblePlatform;
use Mollie\Api\Exceptions\IncompatiblePlatformException;

class CompatibilityChecker
{
Expand All @@ -19,21 +19,21 @@ public static function make(): self
/**
* @return void
*
* @throws IncompatiblePlatform
* @throws IncompatiblePlatformException
*/
public function checkCompatibility()
{
if (! $this->satisfiesPhpVersion()) {
throw new IncompatiblePlatform(
'The client requires PHP version >= '.self::MIN_PHP_VERSION.', you have '.PHP_VERSION.'.',
IncompatiblePlatform::INCOMPATIBLE_PHP_VERSION
throw new IncompatiblePlatformException(
'The client requires PHP version >= ' . self::MIN_PHP_VERSION . ', you have ' . PHP_VERSION . '.',
IncompatiblePlatformException::INCOMPATIBLE_PHP_VERSION
);
}

if (! $this->satisfiesJsonExtension()) {
throw new IncompatiblePlatform(
throw new IncompatiblePlatformException(
"PHP extension json is not enabled. Please make sure to enable 'json' in your PHP configuration.",
IncompatiblePlatform::INCOMPATIBLE_JSON_EXTENSION
IncompatiblePlatformException::INCOMPATIBLE_JSON_EXTENSION
);
}
}
Expand Down
115 changes: 31 additions & 84 deletions src/Exceptions/ApiException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,56 @@
namespace Mollie\Api\Exceptions;

use DateTimeImmutable;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Mollie\Api\Http\Response;
use Throwable;

class ApiException extends MollieException
/**
* Exception thrown when Mollie's API returns an error response.
* This exception always has both a request and a response, where the response
* contains error details from Mollie's API.
*/
class ApiException extends RequestException
{
protected string $plainMessage;

protected ?RequestInterface $request;

protected ?ResponseInterface $response = null;

/**
* ISO8601 representation of the moment this exception was thrown
*/
protected \DateTimeImmutable $raisedAt;

/** @var array<string, \stdClass> */
protected array $links = [];

/**
* @param Response $response The response that caused this exception
* @param string $message The error message
* @param int $code The error code
* @param Throwable|null $previous Previous exception if any
* @throws ApiException
*/
public function __construct(
string $message = '',
int $code = 0,
?RequestInterface $request = null,
?ResponseInterface $response = null,
Response $response,
string $message,
int $code,
?Throwable $previous = null
) {
$this->plainMessage = $message;

$this->raisedAt = new DateTimeImmutable;

$formattedRaisedAt = $this->raisedAt->format(DateTimeImmutable::ATOM);
$message = "[{$formattedRaisedAt}] " . $message;

if (! empty($response)) {
$this->response = $response;

$object = static::parseResponseBody($this->response);

if (isset($object->_links)) {
foreach ($object->_links as $key => $value) {
$this->links[$key] = $value;
}
$object = $response->json();
if (isset($object->_links)) {
foreach ($object->_links as $key => $value) {
$this->links[$key] = $value;
}
}

if ($this->hasLink('documentation')) {
$message .= ". Documentation: {$this->getDocumentationUrl()}";
}

$this->request = $request;
if ($request) {
$requestBody = $request->getBody()->__toString();

if ($requestBody) {
$message .= ". Request body: {$requestBody}";
}
if ($requestBody = $response->getPsrRequest()->getBody()->__toString()) {
$message .= ". Request body: {$requestBody}";
}

parent::__construct($message, $code, $previous);
parent::__construct($response, $message, $code, $previous);
}

public function getDocumentationUrl(): ?string
Expand All @@ -77,51 +65,28 @@ public function getDashboardUrl(): ?string
return $this->getUrl('dashboard');
}

public function getResponse(): ?ResponseInterface
{
return $this->response;
}

public function hasResponse(): bool
{
return (bool) $this->response;
}

/**
* @param string $key
*/
public function hasLink($key): bool
public function hasLink(string $key): bool
{
return array_key_exists($key, $this->links);
}

/**
* @param string $key
*/
public function getLink($key): ?\stdClass
public function getLink(string $key): \stdClass
{
if ($this->hasLink($key)) {
return $this->links[$key];
}

return null;
throw new \RuntimeException("Link '{$key}' not found");
}

/**
* @param string $key
*/
public function getUrl($key): ?string
public function getUrl(string $key): ?string
{
if ($this->hasLink($key)) {
return $this->getLink($key)->href;
if (!$this->hasLink($key)) {
return null;
}

return null;
}

public function getRequest(): ?RequestInterface
{
return $this->request;
$link = $this->getLink($key);
return $link->href;
}

/**
Expand All @@ -133,25 +98,7 @@ public function getRaisedAt(): DateTimeImmutable
}

/**
* @param ResponseInterface $response
*
* @throws ApiException
*/
protected static function parseResponseBody($response): \stdClass
{
$body = (string) $response->getBody();

$object = @json_decode($body);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new self("Unable to decode Mollie response: '{$body}'.");
}

return $object;
}

/**
* Retrieve the plain exception message.
* Retrieve the plain exception message without timestamp and metadata.
*/
public function getPlainMessage(): string
{
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptions/ClientException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace Mollie\Api\Exceptions;

class ClientException extends MollieException {}
class ClientException extends RequestException {}
Loading

0 comments on commit 998f600

Please sign in to comment.