Pular para o conteúdo principal

Advanced Usage

This guide covers advanced features and use cases for PHP Swagger Test.

Supported Content Types​

Request and response bodies are parsed by their Content-Type before being matched against the specification:

Content-TypeParsed into
application/json, any */json or *+json typethe decoded JSON (json_decode())
application/xml, text/xml, any *+xml typethe XML (simplexml_load_string()), matched as an array
multipart/* (requests)the form fields, a file part's value being its content
application/x-www-form-urlencoded (requests)the form fields (parse_str())
nonethe decoded JSON, or the raw string when it is not JSON
anything else (text/html, image/png, ...)the raw string

+json and +xml are the structured syntax suffixes of RFC 6839, e.g. application/problem+json or application/merge-patch+json. An XML body that is not well-formed is kept as a string, so it fails the match. A string body matches a type: string schema, or a media type documented without a schema.

A parsed request body is then validated against the schema of the media type that covers its Content-Type, looked up as for responses: the type itself, its range (multipart/*), then */*. An operation that documents both application/json and multipart/form-data validates each request against its own schema. A request sent with a type the operation does not document fails with NotMatchedException: Content type 'application/x-www-form-urlencoded' not found for POST /pet.

withRequestBody() sends an array as JSON without setting a Content-Type. Such a request is validated against application/json when the operation documents it, then application/xml, text/xml, and otherwise the first documented type.

Response Content Types​

The response is validated against the media type of its Content-Type header, looked up in the operation's content in this order: the type itself (text/html), its range (text/*), then any type (*/*). Parameters such as charset are ignored.

  • A media type documented without a schema accepts any body, so an HTML page documented as content: {"text/html": {}} passes.
  • A media type the operation does not document fails with NotMatchedException: Content type 'text/html' not found for GET 200 /.
  • An XML response (application/xml, text/xml, any +xml type) is validated against its schema as the array it converts to.
  • Without a Content-Type header, the type is guessed from the body: JSON is application/json, any other string text/plain.

Form URL Encoded Requests​

Endpoints consuming application/x-www-form-urlencoded are matched like any other:

<?php
use ByJG\ApiTools\ApiRequester;

$request = new ApiRequester();
$request
->withMethod('POST')
->withPath('/subscribe')
->withRequestHeader(['Content-Type' => 'application/x-www-form-urlencoded'])
->withRequestBody(http_build_query([
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 42,
]))
->expectStatus(200);

$this->sendRequest($request);
observação

A urlencoded body carries only strings. Numeric properties are validated with is_numeric(), so age=42 still matches a schema declaring type: integer — the same way multipart form fields have always behaved.

OpenAPI 3.0:

paths:
/subscribe:
post:
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: string
age:
type: integer

Multipart Form Data and File Uploads​

PHP Swagger Test supports testing endpoints that accept multipart/form-data requests, commonly used for file uploads.

Basic File Upload​

<?php
use ByJG\ApiTools\ApiRequester;

class FileUploadTest extends \ByJG\ApiTools\ApiTestCase
{
public function testUploadImage(): void
{
$imageContent = file_get_contents(__DIR__ . '/fixtures/test-image.jpg');

$request = new ApiRequester();
$request
->withMethod('POST')
->withPath('/pet/1/uploadImage')
->withRequestHeader(['Content-Type' => 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW'])
->withRequestBody(
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" .
"Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n" .
"Content-Type: application/octet-stream\r\n\r\n" .
$imageContent . "\r\n" .
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" .
"Content-Disposition: form-data; name=\"additionalMetadata\"\r\n\r\n" .
"Test image upload\r\n" .
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"
)
->expectStatus(200);

$this->sendRequest($request);
}
}

Simplified File Upload (Base64)​

For simpler testing, you can use base64-encoded content:

public function testUploadImageSimplified(): void
{
$request = new ApiRequester();
$request
->withMethod('POST')
->withPath('/pet/1/uploadImage')
->withRequestBody([
'file' => base64_encode(file_get_contents(__DIR__ . '/fixtures/test-image.jpg')),
'filename' => 'test-image.jpg',
'mimeType' => 'image/jpeg'
])
->expectStatus(200);

$this->sendRequest($request);
}

OpenAPI Specification for File Uploads​

Your OpenAPI spec should define file uploads like this:

OpenAPI 3.0:

paths:
/pet/{petId}/uploadImage:
post:
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
additionalMetadata:
type: string

Swagger 2.0:

paths:
/pet/{petId}/uploadImage:
post:
consumes:
- multipart/form-data
parameters:
- in: formData
name: file
type: file
required: true
- in: formData
name: additionalMetadata
type: string

Custom Server URLs and Environments​

Overriding Server URLs​

send() builds the request URI from the specification: the scheme, host, port and path of the first servers entry (OpenAPI 3.x) or of schemes/host/basePath (Swagger 2.0) replace the ones of the request. The scheme and host of the request are used only when the specification does not define them; when it defines no host, the port of the request is kept too (servers: [{url: /v2}] with a request to http://localhost:8080/pet sends http://localhost:8080/v2/pet). To test another environment, choose it in the specification, for example with a server variable (below), or load a specification per environment (see Environment-Specific Configuration).

Using Server Variables (OpenAPI 3.0)​

For OpenAPI 3.0, you can set server variables dynamically:

# openapi.json
servers:
- url: 'https://{environment}.example.com/v1'
variables:
environment:
default: api
enum:
- api
- api.dev
- api.staging
<?php
use ByJG\ApiTools\OpenApi\OpenApiSchema;

class ServerVariableTest extends \ByJG\ApiTools\ApiTestCase
{
public function testWithDevelopmentServer(): void
{
$schema = OpenApiSchema::fromFile(__DIR__ . '/openapi.json');

// Set server variable
$schema->setServerVariable('environment', 'api.dev');

$this->setSchema($schema);

// Now getServerUrl() returns 'https://api.dev.example.com/v1'
$this->assertEquals('https://api.dev.example.com/v1', $schema->getServerUrl());

$request = new ApiRequester();
$request->withMethod('GET')->withPath('/pet/1');

$this->sendRequest($request);
}
}

Working with PSR-7 Requests​

PHP Swagger Test is built on PSR-7, allowing seamless integration with PSR-7 compatible frameworks.

Building Requests from PSR-7​

<?php
use ByJG\WebRequest\Psr7\Request;
use ByJG\Util\Uri;
use ByJG\WebRequest\Psr7\MemoryStream;
use ByJG\ApiTools\ApiRequester;

class Psr7IntegrationTest extends \ByJG\ApiTools\ApiTestCase
{
public function testWithPsr7Request(): void
{
// Build a complete PSR-7 request
$uri = new Uri('https://api.example.com/pet/1?detailed=true');

$psr7Request = Request::getInstance($uri)
->withMethod('POST')
->withHeader('Content-Type', 'application/json')
->withHeader('Authorization', 'Bearer token123')
->withBody(new MemoryStream(json_encode([
'name' => 'Fluffy',
'status' => 'available'
])));

// Use it with ApiRequester
$request = new ApiRequester();
$request->withPsr7Request($psr7Request);

$response = $this->sendRequest($request);

// Response is also PSR-7
$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue($response->hasHeader('Content-Type'));
}
}

Modifying PSR-7 Requests​

public function testModifyPsr7Request(): void
{
// Build the PSR-7 request yourself and hand it to the requester.
// (The requester keeps it in the protected $psr7Request property, reachable from a subclass.)
$psr7 = Request::getInstance(new Uri('/pet/1'))
->withMethod('GET')
->withHeader('X-Custom-Header', 'value')
->withHeader('Accept-Language', 'en-US');

$request = new ApiRequester();
$request->withPsr7Request($psr7);

$this->sendRequest($request);
}

Custom HTTP Clients​

You can use custom HTTP clients by extending AbstractRequester.

Using Guzzle​

<?php
use ByJG\ApiTools\AbstractRequester;
use GuzzleHttp\Client;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleRequester extends AbstractRequester
{
private Client $client;

public function __construct()
{
parent::__construct();
$this->client = new Client([
'timeout' => 30,
'verify' => false, // Disable SSL verification for testing
]);
}

protected function handleRequest(RequestInterface $request): ResponseInterface
{
return $this->client->send($request, [
'http_errors' => false, // Don't throw exceptions on 4xx/5xx
]);
}
}

Usage:

class MyTest extends \ByJG\ApiTools\ApiTestCase
{
public function testWithGuzzle(): void
{
$request = new GuzzleRequester(); // Use custom requester
$request
->withSchema($this->schema)
->withMethod('GET')
->withPath('/pet/1');

$this->sendRequest($request);
}
}

Using Symfony HttpClient​

<?php
use ByJG\ApiTools\AbstractRequester;
use Symfony\Component\HttpClient\Psr18Client;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class SymfonyRequester extends AbstractRequester
{
private Psr18Client $client;

public function __construct()
{
parent::__construct();
$this->client = new Psr18Client();
}

protected function handleRequest(RequestInterface $request): ResponseInterface
{
return $this->client->sendRequest($request);
}
}

Testing Authenticated Endpoints​

Bearer Token Authentication​

<?php
class AuthenticatedTest extends \ByJG\ApiTools\ApiTestCase
{
private function getAuthToken(): string
{
// Get token from authentication endpoint or fixture
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
}

public function testWithBearerToken(): void
{
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/user/profile')
->withRequestHeader([
'Authorization' => 'Bearer ' . $this->getAuthToken()
])
->expectStatus(200);

$this->sendRequest($request);
}
}

API Key Authentication​

public function testWithApiKey(): void
{
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/pet/1')
->withRequestHeader([
'X-API-Key' => getenv('API_KEY')
])
->expectStatus(200);

$this->sendRequest($request);
}

Basic Authentication​

public function testWithBasicAuth(): void
{
$username = 'user';
$password = 'pass';
$credentials = base64_encode("$username:$password");

$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/admin/users')
->withRequestHeader([
'Authorization' => "Basic $credentials"
])
->expectStatus(200);

$this->sendRequest($request);
}

Reusable Authentication Helper​

<?php
abstract class AuthenticatedTestCase extends \ByJG\ApiTools\ApiTestCase
{
protected function createAuthenticatedRequest(string $method, string $path): ApiRequester
{
$request = new ApiRequester();
$request
->withMethod($method)
->withPath($path)
->withRequestHeader([
'Authorization' => 'Bearer ' . $this->getAuthToken()
]);

return $request;
}

protected function getAuthToken(): string
{
// Implement token retrieval logic
static $token = null;

if ($token === null) {
// Authenticate once and cache
$authRequest = new ApiRequester();
$authRequest
->withMethod('POST')
->withPath('/auth/login')
->withRequestBody([
'username' => 'testuser',
'password' => 'testpass'
]);

$response = $this->sendRequest($authRequest);
$body = json_decode((string)$response->getBody(), true);
$token = $body['token'];
}

return $token;
}
}

// Use in tests
class UserApiTest extends AuthenticatedTestCase
{
public function testGetProfile(): void
{
$request = $this->createAuthenticatedRequest('GET', '/user/profile');
$this->sendRequest($request);
}
}

Testing Pagination​

<?php
class PaginationTest extends \ByJG\ApiTools\ApiTestCase
{
public function testPaginatedEndpoint(): void
{
$allItems = [];
$page = 1;
$hasMore = true;

while ($hasMore) {
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/pets')
->withQuery([
'page' => $page,
'limit' => 10
])
->expectStatus(200);

$response = $this->sendRequest($request);
$body = json_decode((string)$response->getBody(), true);

$allItems = array_merge($allItems, $body['items']);

$hasMore = $body['hasMore'] ?? false;
$page++;

// Safety limit
if ($page > 10) {
break;
}
}

$this->assertGreaterThan(0, count($allItems));
}
}

Testing Rate Limiting​

<?php
use ByJG\ApiTools\Exception\StatusCodeNotMatchedException;

class RateLimitTest extends \ByJG\ApiTools\ApiTestCase
{
public function testRateLimiting(): void
{
$requests = 0;
$rateLimited = false;

for ($i = 0; $i < 100; $i++) {
try {
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/pets')
->expectStatus(200);

$this->sendRequest($request);
$requests++;

} catch (StatusCodeNotMatchedException $e) {
// getBody() is the raw response body; the status is in the message
if (str_contains($e->getMessage(), 'got 429')) {
$rateLimited = true;
break;
}
throw $e;
}

usleep(10000); // 10ms delay
}

$this->assertTrue(
$rateLimited,
"Expected rate limiting after $requests requests"
);
}
}

Testing with Null Values (Swagger 2.0)​

Swagger 2.0 doesn't explicitly support null values. Use allowNullValues to handle this:

<?php
use ByJG\ApiTools\Base\Schema;

class NullValueTest extends \ByJG\ApiTools\ApiTestCase
{
public function setUp(): void
{
// Allow null values in responses
$schema = Schema::fromFile(__DIR__ . '/swagger.json', allowNullValues: true);
$this->setSchema($schema);
}

public function testResponseWithNulls(): void
{
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/pet/1');

$response = $this->sendRequest($request);

$body = json_decode((string)$response->getBody(), true);

// These nulls won't cause validation errors
$this->assertNull($body['category'] ?? null);
}
}

Environment-Specific Configuration​

<?php
class ConfigurableTest extends \ByJG\ApiTools\ApiTestCase
{
private function getSpecPath(): string
{
$env = getenv('TEST_ENV') ?: 'local';

return match($env) {
'local' => __DIR__ . '/specs/openapi.local.json',
'dev' => __DIR__ . '/specs/openapi.dev.json',
'staging' => __DIR__ . '/specs/openapi.staging.json',
'production' => __DIR__ . '/specs/openapi.prod.json',
default => __DIR__ . '/specs/openapi.json',
};
}

public function setUp(): void
{
$schema = Schema::fromFile($this->getSpecPath());
$this->setSchema($schema);
}

public function testEndpoint(): void
{
// Test runs against environment-specific spec
$request = new ApiRequester();
$request
->withMethod('GET')
->withPath('/pet/1');

$this->sendRequest($request);
}
}

Run tests:

# Test against local spec
TEST_ENV=local vendor/bin/phpunit

# Test against staging spec
TEST_ENV=staging vendor/bin/phpunit

Testing Webhooks and Callbacks​

While OpenAPI callbacks are not fully implemented, you can test webhook endpoints:

<?php
class WebhookTest extends \ByJG\ApiTools\ApiTestCase
{
public function testWebhookEndpoint(): void
{
// Simulate webhook payload
$webhookPayload = [
'event' => 'pet.updated',
'petId' => 123,
'timestamp' => time(),
'data' => [
'name' => 'Fluffy',
'status' => 'sold'
]
];

$request = new ApiRequester();
$request
->withMethod('POST')
->withPath('/webhooks/pet-updates')
->withRequestHeader([
'X-Webhook-Signature' => hash_hmac('sha256', json_encode($webhookPayload), 'secret')
])
->withRequestBody($webhookPayload)
->expectStatus(200);

$this->sendRequest($request);
}
}