Component: OpenAPI¶
Jane OpenAPI is a library to generate, in PHP, an HTTP client and its associated models and serializers from a OpenAPI specification: version 2 or 3. Jane supports both OpenAPI v2 & v3. Depending on your OpenAPI version, the command line will detect which version to use and if this version is actually installed in your dependencies.
Installation¶
Jane supports both OpenAPI v2 & v3. Depending on your OpenAPI version, the command line will detect which version to use and if this version is actually installed in your dependencies.
You have to add the generation library as a dev dependency. This library contains a lot of dependencies, to be able
to generate code, which are not needed on runtime. However, the generated code depends on other libraries and a few
classes that are available through the runtime package. It is mandatory to add the runtime dependency as a requirement.
Choose your library depending on OpenAPI version you need (you can even install both if you want):
# OpenAPI 2
composer require --dev jane-php/open-api-2
composer require jane-php/open-api-runtime
# OpenAPI 3
composer require --dev jane-php/open-api-3
composer require jane-php/open-api-runtime
With Symfony ecosystem, we created a recipe to make it easier to use Jane. You just have to allow contrib recipes before installing our packages:
composer config extra.symfony.allow-contrib true
Then when installing jane-php/open-api-*, it will add all the required files:
bin/jane-open-api-generate: a binary file to run JSON Schema generation based onconfig/jane/open-api.phpconfiguration;config/jane/open-api.php: your Jane configuration (see "Configuration file");config/packages/open-api.yaml: Symfony Serializer configured to be optimized for Jane.
By default, generated code is not formatted, to make it compliant to PSR2 standard and others coding style formats, you can add the PHP CS Fixer library to your dev dependencies (and it makes it easier to debug!):
composer require --dev friendsofphp/php-cs-fixer
Generating a Client¶
This library provides a PHP console application to generate the Model. You can use it by executing the following command at the root of your project:
php vendor/bin/jane-openapi generate
This command will try to read a config file named .jane-openapi located on the current working directory. However,
you can name it as you like and use the --config-file option to specify its location and name:
php vendor/bin/jane-openapi generate --config-file=jane-openapi-configuration.php
Note
If you are using Symfony recipe, this command is embbeded in the bin/jane-open-api-generate binary file, you only
have to run it to make it work 🎉
Note
No others options can be passed to the command. Having a config file ensure that a team working on the project always use the same set of parameters and, when it changes, give vision of the new option(s) used to generate the code.
Tip
If you have a really big specification and want to optimize your generation time, you can disable garbage collector
during generation, you can read more about it on
Scrutinizer blog post. To do that,
use Jane as following: php -d zend.enable_gc=0 vendor/bin/jane-openapi generate.
Configuration file¶
The configuration file consists of a simple PHP script returning an array:
return [
'openapi-file' => __DIR__ . '/open-api.json',
'namespace' => 'Vendor\Library\Generated',
'directory' => __DIR__ . '/generated',
];
This example shows the minimum configuration required to generate a client:
openapi-file: Specify the location of your OpenApi file, it can be a local file or a remote onehttps://my.domain.com/my-api.json. It can also be ayamlfile.namespace: Root namespace of all of your generated codedirectory: Directory where the code will be generated
Given this configuration, you will need to add the following configuration to composer, in order to load the generated files:
"autoload": {
"psr-4": {
"Vendor\\Library\\Generated\\": "generated/"
}
}
Options¶
Other options are available to customize the generated code:
reference: A boolean which indicate to add the support for JSON Reference into the generated code.date-format: A date-time format to specify how the generated code should encode and decode\DateTimeobject to string. This option is only for formatdate-time.full-date-format: A date format to specify how the generated code should encode and decode\DateTimeobject to string. This option is only for formatdate.date-prefer-interface: The\DateTimeInterfaceis the base of every\DateTimerelated action. This makes it more compatible with other DateTime libraries like Carbon. This option replace\DateTimereturns with\DateTimeInterface, it's disabled by default.date-input-format: During denormalization (from array to object), we may have a different format than the output format. This option allows you to specify which format you want. By default it will takedate-formatconfiguration.strict: A boolean which indicate strict mode (true by default), not strict mode generate more permissive client not respecting some standards (nullable field as an example) client.use-fixer: A boolean which indicate if we make a first cs-fix after code generation, is disabled by default.fixer-config-file: A string to specify where to find the custom configuration for the cs-fixer after code generation, will remove all Jane default cs-fixer default configuration.clean-generated: A boolean which indicate if we clean generated output before generating new files, is enabled by default.use-cacheable-supports-method: A boolean which indicate if we useCacheableSupportsMethodInterfaceinterface to improve caching performances when used with Symfony Serializer.skip-null-values: When having nullable properties, you can enforce normalization to skip theses properties even if they are nullable. This option allows you to not have theses properties when they're not set (null). By default it is enabled.skip-required-fields: If your model has required fields, this option allows you to skip the required behavior that forces them to be present during denormalization. By default it is disabledvalidation: Will enable validation following JSON Schema validation specification. By default it is disabled. You can read more about it on the dedicated guide: Validation guide.include-null-value: Will enable a way to manage null values. By default it is enabled.whitelisted-paths: This option allows you to generate only needed endpoints and related models. Be carefull, that option will filter models used by whitelisted endpoints and generate model & normalizer only for them. Here is some examples about how to use it:
return [
// your usual configuration ...
'whitelisted-paths' => [
'\/foo$',
['\/foo\/(bar|baz)'],
['\/foo$', 'GET'],
['\/foo$', ['POST']],
['\/foo$', ['POST', 'PUT']]
],
];
There is many ways to use it, first you atleast need a regex defining which endpoint is whitelisted. This endpoint
can be either a string or in an array. If you don't provide any HTTP method, we will just accept any methods, but
you can provide either a string or array as second argument to specify which method you accept.
- endpoint-generator: Generator Class which can specify custom endpoint interface & corresponding trait (this class
should extends \Jane\Component\OpenApi3\Generator\EndpointGenerator)
- custom-query-resolver: This option allows you to customize the query parameter normalizer for each of the API
endpoint with a userland callback. Here is all possible combinations::
use App\BoolCustomQueryResolver;
use App\IntCustomQueryResolver;
use App\BarCustomQueryResolver;
use App\BazCustomQueryResolver;
return [
// your usual configuration ...
'custom-query-resolver' => [
'__type' => [
'bool' => BoolCustomQueryResolver::class,
'int' => IntCustomQueryResolver::class,
],
'/foo' => [
'get' => [
'bar' => BarCustomQueryResolver::class,
'baz' => BazCustomQueryResolver::class,
],
'post' => [
'bar' => BarCustomQueryResolver::class,
],
],
],
];
There are many ways to use it. You can either use the __type key to specify a custom query normalizer for a
given type (bool, int, string, ...) and give it your class that contains the custom normalizer by
extending the generated runtime CustomQueryResolver class. You can also filter the usage of your custom
normalizer by giving the exact path, method and parameter name where you want to apply it.
- throw-unexpected-status-code: Will return a UnexpectedStatusCodeException if nothing has been matched during
the transformation of the Endpoint body (including described exceptions). By default, it's disabled.
- custom-string-format-mapping: This option allows you to specify in which class a string property will be
deserialized according to it's format option. It can be used to customize a date-time field, or to add non supported
formats. More details in the dedicated section.
Using a generated client¶
Generating a client will produce same classes as the JSON Schema library:
- Model files in the
Modelnamespace - Normalizer files in the
Normalizernamespace - A
JaneObjectNormalizerclass in theNormalizernamespace
Furthermore, it generates:
- Endpoints files in the
Endpointnamespace, each API Endpoint will generate a class containing all the logic to go from Object to Request, and from Response to Object with the generated Normalizer Clientfile in the root namespace containing all API endpoints
Creating the API Client¶
Generated Client class have a static method create which act like a factory to create your Client:
$apiClient = Vendor\Library\Generated\Client::create();
Note
If you are using Symfony recipe, the client will be autowired. So you can use it anywhere by using your Client class
Note
Optionally, you can pass a custom HttpClient respecting the PSR18 Client
standard. If you which to use the constructor to reuse existing instances, sections below describe the 4 services
used by it and how to create them.
Creating the Http Client¶
The main dependency on the Client class is an HTTP client respecting the PSR18
client standard. We highly recommend you to read the PSR18 specification. This
HTTP client MAY redirect on a 3XX responses (depend on your API), but it MUST not throw errors on 4XX and 5XX responses,
as this can be handle by the generated code directly.
Recommended way of creating an HTTP Client is by using the discovery library to create the client::
$httpClient = Http\Discovery\Psr18ClientDiscovery::find();
This allows user of the API to use any client respecting the standard.
Tip
You can use clients such as Symfony HttpClient as PSR18 client.
Creating the Request Factory¶
The generated endpoints will also need a factory to transform parameters and object of the endpoint to a PSR7 Request.
Like the HTTP Client, it is recommended to use the discovery library to create it:
$requestFactory = Http\Discovery\Psr17FactoryDiscovery::findRequestFactory();
Creating the Serializer¶
Like in JSON Schema component, creating a serializer is done by using the
JaneObjectNormalizer class:
$normalizers = [
new \Symfony\Component\Serializer\Normalizer\ArrayDenormalizer(),
new \Vendor\Library\Generated\Normalizer\JaneObjectNormalizer(),
];
$serializer = new \Symfony\Component\Serializer\Serializer($normalizers, [new \Symfony\Component\Serializer\Encoder\JsonEncoder()]);
$serializer->deserialize('{...}');
With Symfony ecosystem, you just have to use the recipe and all the configuration will be added automatically. This serializer will be able to encode and decode every data respecting your OpenAPI specification thanks to autowiring of the generated normalizers.
Creating the Stream Factory¶
The generated endpoints will also need a service to transform body parameters like resource or string into
PSR7 Stream when uploading file (multipart form).
Like the HTTP Client and Request Factory, it is recommended to use the discovery library to create it:
$streamFactory = Http\Discovery\Psr17FactoryDiscovery::findStreamFactory();
Using the API Client¶
Generated code has complete PHPDoc comment on each method, which should correctly describe
the endpoint. Method names for each endpoint depends on the operationId property of the OpenAPI specification. And if
not present it will be generated from the endpoint path:
$apiClient = Vendor\Library\Generated\Client::create();
// Operation id being listFoo
$foos = $apiClient->listFoo();
Also depending on the parameters of the endpoint, it may have 2 to more arguments.
Last parameter of each endpoint, allows to specify which type of data the method must return. By default, it will try to return an object depending on the status code of your response. But you can force the method to return a PSR7 Response object:
$apiClient = Vendor\Library\Generated\Client::create();
// First argument is an empty list of parameters, second one being the return type
$response = $apiClient->listFoo([], Vendor\Library\Generated\Client::FETCH_RESPONSE);
This allow to do custom work when the API does not return standard JSON body.
Host and basePath support¶
Jane OpenAPI will never generate the complete url with the host and the base path for an endpoint. Instead, it will only do a request on the specified path.
If host and/or base path is present in the specification it is added, via the PluginClient, AddHostPlugin and
AddPathPlugin thanks to php-http plugin system_ when using the static create.
This allow you to configure different host and base path given a specific environment / server, which may defer when in test, preprod and production environment.
Jane OpenAPI will always try to use https if present in the scheme (or if there is no scheme). It will use the first scheme
present if https is not present.
Having custom plugins¶
If you want to support more behavior such as authentication or other stuff that need a plugin, you can pass them
through the second argument of the static create method.
Authentication¶
We do generate a plugin for each authentication method declared in your scheme. It does support:
apiKeyin header & query for both OpenAPI v2 & v3- HTTP Basic & Bearer for OpenAPI v3
Quick example of how your authentication definition could look (OpenAPI v3):
components:
securitySchemes:
BasicAuth:
type: http
scheme: basic
BearerAuth:
type: http
scheme: bearer
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
When your OpenAPI definition contains it, Jane will generate a Authentication namespace that contains all plugins you
need for your API.
Then you give all your authentication plugins to Jane\Component\OpenApiRuntime\Client\Plugin\AuthenticationRegistry.
And finally you can pass it to your Jane Client (only if you let Jane make a HTTP Client for you, otherwise this second
parameters is ignored).
An example Authentification directory:

This AuthenticationRegistry class is used to match security scopes in your API, if an Endpoint require a certain
authentication method, then it will use it. You need to have security fields correctly made in your scheme in order
to use this class. If they're not set, you can simply pass the authentication plugin to your Jane Client.
Here is how you can use it:
$authenticationRegistry = new AuthenticationRegistry([new ApiKeyAuthentication($this->apiKey)]);
$client = Client::create(null, [$authenticationRegistry]);
$foo = $client->foo();
You can replace Client::create first argument with your custom HttpClient if needed as usual.
Extending the Client¶
Some endpoints need sometimes custom implementation that were not possible to generate through the OpenAPI specification. Jane OpenAPI try to be nice with this and each specific behavior of an API call has been seprated into different methods which are public or protected.
As an exemple you may want to encode in base64 a specific query parameter of an Endpoint. First step is to create your own Endpoint extending the generated one:
namespace Vendor\Library\Generated\Endpoint;
use Vendor\Library\Generated\Endpoint\FooEndpoint as BaseEndpoint;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FooEndpoint extends BaseEndpoint
{
protected function getQueryOptionsResolver(): OptionsResolver
{
$optionsResolver = parent::getQueryOptionsResolver();
$optionsResolver->setNormalizer('bar', function (Options $options, $value) {
return base64_encode($value);
});
return $optionsResolver;
}
}
Once this endpoint is generated, you need to tell your Client to use yours endpoint instead of the Generated one. For that you can extends the generated client and override the method that use this endpoint:
namespace Vendor\Library\Generated;
use Vendor\Library\Generated\Client as BaseClient;
use Vendor\Library\Generated\Endpoint\FooEndpoint;
class Client extends BaseClient
{
public function getFoo(array $queryParameters = [], $fetch = self::FETCH_OBJECT)
{
return $this->executePsr7Endpoint(new FooEndpoint($queryParameters), $fetch);
}
}
Then you will need to use your own client instead of the generated one. To extends other parts of the endpoint you can look at the generated code.
Custom string formats¶
Jane support some strings format, but it can't support all of them because it's an open keyword.
You may want to serialize a property to an UUID, or have a specific datetime format for a field (a datetime format that
is not the same as the one configured with date-format or full-date-format.
To do so, you need to provide:
- while generating the client: an associative array for the key
custom-string-format-mapping - at runtime: one or more Normalizer (which implement
Symfony\Component\Serializer\Normalizer\NormalizerInterface)
Example¶
Configuration file:
return [
'json-schema-file' => __DIR__ . '/json-schema.json',
'root-class' => 'MyModel',
'namespace' => 'Vendor\Library\Generated',
'directory' => __DIR__ . '/generated',
'custom-string-format-mapping' => [
'uuid' => \Symfony\Component\Uid\UuidV4::class
]
];
Your OpenAPI schema:
openapi: "3.0.0"
info:
version: 1.0.0
title: Example
paths:
/some-path:
get:
summary: Get something
operationId: getSomething
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Something"
components:
schemas:
Something:
type: object
required:
- id
- uuid
properties:
id:
type: 'integer'
uuid:
type: 'string'
# the following keyword is important
format: 'uuid'
Usage of the generated client:
$client = \Vendor\Library\Generated\Client::create(
$httpClient,
[], // additional http client plugins
// additional normalizers
[
new \Symfony\Component\Serializer\Normalizer\UidNormalizer()
]
);