Jane: Tools for generating PHP Code¶
Jane is a set of libraries to generate Models & API Clients based on JsonSchema / OpenAPI specs by following high quality PHP code guidelines and respecting common & advanced PSR.
Backwards compatibility¶
Backwards compatiblity is an important topic. Those libraries follow Semver, so backwards compatibility will only break between major versions. This library may use deprecations notices to inform you of the change, but it’s a low probability, you should always check the CHANGELOG when switching to a new major version.
JsonSchema and OpenAPI¶
Those libraries generate code and should not be used in runtime. Also, there is no need to extends or use this code in another libraries. The only thing used, is the command line.
So there is no BC promise on those libraries, you can consider that everything is internal. The only BC promise is about the command line, and the generated code.
Generated Code¶
Code generated fall under our BC Promise, but only the public and protected API of the generated code. When a method of a class is generated, its signature will not change with minor release, but it’s implementation may change, however a private method can have its signature updated. Behavior of the implementation should not change between minor releases unless behavior is buggy.
No class will be removed between minor versions, but there can be new classes added.
Runtime Libraries¶
JsonSchema Runtime and OpenAPI Runtime libraries have a standard BC Promise.
Internal¶
This documentation describes how JsonSchema & OpenAPI Jane libraries work to generate the code. It is mainly oriented for people wanting to contribute to theses libraries.
Theses libraries is based on 3 different steps:
Guessing¶
First step is to guess a set of metadata given a specification (JsonSchema or OpenAPI at the time of writing this).
To do so, it will read the specification, transform it into objects and pass it to guessers implementing one of
the GuesserInterface
.
Each guesser tell if it supports the current specification and returns metadata. Occasionally, it will try to guess sub objects of the specification.
Analyzing¶
Once all metadata are guessed, they are passed to a set of generators implementing the GeneratorInterface
given a
Context
.
Then, each generator will analyze the metadata and create PHP code by using the PHP Parser Library. Using the library improves the flexibility to create complex code, as using a template generator solution.
Context
provides a lots of functions to generate code, like using unique variable name in a scope or adding generated file.
Generation¶
When the code is ready, the Context
is read to generate PHP files and optionally format it with PHP CS Fixer if available.
Json Schema¶
Jane JsonSchema is a library to generate models and serializers in PHP from a JSON Schema draft v4.
Installation¶
Add this library with composer as a dev
dependency:
composer require --dev jane-php/json-schema "^5.0"
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 highly recommended to add the runtime dependency as a requirement through composer:
composer require jane-php/json-schema-runtime "^5.0"
By default, generated code is not formatted, to make it compliant to PSR2 standard and others format norms, 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 Model¶
This library provided 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 generate
This command will try to read a config file named .jane
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 generate --config-file=jane-configuration.php
Note
No others options can be passed to this 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 used to generate the code.
Configuration file¶
The configuration file consists of a simple PHP script returning an array:
<?php
return [
'json-schema-file' => __DIR__ . '/json-schema.json',
'root-class' => 'MyModel',
'namespace' => 'Vendor\Library\Generated',
'directory' => __DIR__ . '/generated',
];
This example shows the minimum configuration required to generate a Model:
json-schema-file
: Specify the location of your json schema file, it can be a local file or a remote onehttps://my.domain.com/my-schema.json
root-class
: The root class of the root object defined in your json schema, if there is no property on the root object it will not be usednamespace
: Root namespace of all of your generated codedirectory
: Directory where the code will be generated at
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 format to specify how the generated code should encode and decode\DateTime
object to stringuse-fixer
: A boolean which indicate if we make a first cs-fix after code generationfixer-config-file
: A string to specify where to find the custom configuration for the cs-fixer after code generationuse-cacheable-supports-method
: A boolean which indicate if we useCacheableSupportsMethodInterface
interface to improve caching performances when used with Symfony Serializer.
Multi schemas¶
Jane JsonSchema can also generate multiple schemas at the same time with different namespaces and directories, allowing to handle JSON References on others schemas.
See Multi schemas generation for more information
Using a generated Model¶
This library generates basics P.O.P.O. objects (Plain Old PHP Objects) with a bunch of setters / getters. It also generates all normalizers to handle denormalization from a json string, and normalization.
All normalizers respect the Symfony\Component\Serializer\Normalizer\NormalizerInterface
and
Symfony\Component\Serializer\Normalizer\DenormalizerInterface
from the Symfony Serializer Component.
It also generates a NormalizerFactory
class having a static function create
returning an array of all
normalizers.
Given this configuration:
<?php
return [
'json-schema-file' => __DIR__ . '/json-schema.json',
'root-class' => 'MyModel',
'namespace' => 'Vendor\Library\Generated',
'directory' => __DIR__ . '/generated',
];
You will have to do this:
<?php
$normalizers = Vendor\Library\Generated\Normalizer\NormalizerFactory::create();
$encoders = [new Symfony\Component\Serializer\Encoder\JsonEncoder(
new Symfony\Component\Serializer\Encoder\JsonEncode([Symfony\Component\Serializer\Encoder\JsonEncode::OPTIONS => \JSON_UNESCAPED_SLASHES]),
new Symfony\Component\Serializer\Encoder\JsonDecode([Symfony\Component\Serializer\Encoder\JsonDecode::ASSOCIATIVE => false])),
];
$serializer = new Symfony\Component\Serializer\Serializer($normalizers, $encoders);
$serializer->deserialize('{...}');
This serializer will be able to encode and decode every data respecting your json schema specification.
Note
Take note that we don’t use classic defaults for JsonEncode
and JsonDecode
. Using JSON_UNESCAPED_SLASHES
only makes sense if you can have JSON References in your data (not specification). However, using false
for
JsonDecode
(which means not using associative array but \stdClass
instead) is mandatory.
As an example of why it’s mandatory, a JSON Schema could contain the following valid specification:
{
"type": "object",
"properties": {
"foo": {
"type": ["array", "object"]
}
}
}
When using associative array, it would be tricky (but feasible) to deal with data inside the array or object (need to detect if all keys are numerical). The main problem comes when dealing with an empty array or object. In this case, there is no possibility to know if it was an array or object, and in some cases, decoding and recoding this value (with no modification) will change the data.
Multi schemas generation¶
Jane JsonSchema allows to generate multiple schemas at the same time with different namespaces and directories to handle JSON References on others schemas.
Configuration¶
In order to use this feature, configuration of the .jane
file will require a mapping of JSON Schema specification file
linked to a root class, namespace and directory.
As an example you may have this:
<?php
return [
'mapping' => [
__DIR__ . '/schema1.json' => [
'root-class' => 'Foo',
'namespace' => 'Vendor\Library\FooSchema',
'directory' => __DIR__ . '/generated/Schema1',
],
__DIR__ . '/schema2.json' => [
'root-class' => 'Bar',
'namespace' => 'Vendor\Library\BarSchema',
'directory' => __DIR__ . '/generated/Schema2',
],
],
];
Using this configuration, Jane JsonSchema will generate all class of the schema1.json
and schema2.json
specification.
Also, all references between both schemas will use the specific namespace.
As an example, given that you have the Foo
object in schema1.json
:
{
"type": "object",
"properties": {
"foo": { "type": "string" }
}
}
And the Bar
one in schema2.json
:
{
"type": "object",
"properties": {
"bar": { "$ref": "schema1.json#" }
}
}
The property bar
of the Bar
object will reference the Vendor\Library\Schema1\Foo
class.
Note
If we don’t specify the schema1.json
in this configuration, Jane JsonSchema will still fetch the specification
and generate the Foo
class. However, it will be under the same namespace (Vendor\Library\BarSchema
),
and will have BarBar
as the class name, instead of the Foo
one.
Usage¶
In this case, Jane JsonSchema will generate two distinct NormalizerFactory
, to be able to use references between
schemas, you will only need to merge normalizers:
<?php
$normalizers = array_merge(
\Vendor\Library\FooSchema\Normalizer\NormalizerFactory::create(),
\Vendor\Library\BarSchema\Normalizer\NormalizerFactory::create()
);
OpenAPI¶
Jane OpenAPI is a library to generate, in PHP, an http client and its associated models and serializers from a OpenAPI specification: version 3.
Compatibility¶
Jane supports OpenAPI v2 & v3. Depending on your OpenAPI version, you should use following Jane version:
OpenAPI | Jane |
---|---|
v3 | ^5.0 |
v2 | ^4.0 |
Installation¶
Add this library with composer as a dev
dependency (replace version depending on what you need):
composer require --dev jane-php/open-api "^5.0"
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 highly recommended to add the runtime dependency as a requirement through composer:
composer require jane-php/open-api-runtime "^5.0"
By default, generated code is not formatted, to make it compliant to PSR2 standard and others format norms, 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 provided 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
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 used to generate the code.
Configuration file¶
The configuration file consists of a simple PHP script returning an array:
<?php
return [
'openapi-file' => __DIR__ . '/openapi.json',
'namespace' => 'Vendor\Library\Api',
'directory' => __DIR__ . '/generated',
'client' => 'psr18',
];
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 ayaml
file.namespace
: Root namespace of all of your generated codedirectory
: Directory where the code will be generatedclient
: Client to generate (httplug
orpsr18
,httplug
is deprecated and will be removed in Jane v6.0.0)
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\\Api\\": "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 format to specify how the generated code should encode and decode\DateTime
object to stringstrict
: 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 generationfixer-config-file
: A string to specify where to find the custom configuration for the cs-fixer after code generationuse-cacheable-supports-method
: A boolean which indicate if we useCacheableSupportsMethodInterface
interface to improve caching performances when used with Symfony Serializer.
Using a generated client¶
Generating a client will produce same classes as the Json Schema library:
- Model files in the
Model
namespace- Normalizer files in the
Normalizer
namespace- A
NormalizerFactory
class in theNormalizer
namespace
Furthermore, it generates:
- Endpoints files in the
Endpoint
namespace, each API Endpoint will generate a class containing all the logic to go from Object to Request, and from Response to Object- Client file 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:
<?php
$apiClient = Vendor\Library\Generated\Client::create();
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:
<?php
$httpClient = Http\Discovery\Psr18ClientDiscovery::find();
This allows user of the API to use any client respecting the standard.
Hint
You can use clients such as Symfony HttpClient as PSR18 client.
Creating the Serializer¶
Like in Using a generated Model, creating a serializer is done by using the NormalizerFactory
class:
<?php
$normalizers = Vendor\Library\Generated\Normalizer\NormalizerFactory::create();
$encoders = [new Symfony\Component\Serializer\Encoder\JsonEncoder(
new Symfony\Component\Serializer\Encoder\JsonEncode(JSON_UNESCAPED_SLASHES),
new Symfony\Component\Serializer\Encoder\JsonDecode(false))
];
$serializer = new Symfony\Component\Serializer\Serializer($normalizers, $encoders);
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:
<?php
$requestFactory = Http\Discovery\Psr17FactoryDiscovery::findRequestFactory();
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:
<?php
$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:
<?php
$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.
Example¶
In this section, we will see a working example of OpenApi v3 client onto a simple API that gives facts about cats and comment it.
Warning
We suggest you to read Generating a Client and Using a generated client first to understand this page more easily.
You can find the fully working example on the following link: https://github.com/janephp/openapi3-example
OpenAPI schema¶
First, we need a valid OpenAPI schema. You can use tool such as Stoplight or other OpenApi designer.
I choosed to represent CatFacts API within this example:
openapi: 3.0.0
info:
version: 1.0.0
title: 'CatFacts API'
servers:
- url: https://cat-fact.herokuapp.com
paths:
/facts/random:
get:
operationId: randomFact
responses:
200:
description: 'Get a random `Fact`'
content:
application/json:
schema:
$ref: '#/components/schemas/Fact'
components:
schemas:
Fact:
type: object
properties:
_id:
type: string
description: 'Unique ID for the `Fact`'
__v:
type: integer
description: 'Version number of the `Fact`'
user:
type: string
description: 'ID of the `User` who added the `Fact`'
text:
type: string
description: 'The `Fact` itself'
updatedAt:
type: string
format: date-time
description: 'Date in which `Fact` was last modified'
sendDate:
type: string
description: 'If the `Fact` is meant for one time use, this is the date that it is used'
deleted:
type: boolean
description: 'Weather or not the `Fact` has been deleted (Soft deletes are used)'
source:
type: string
description: 'Can be `user` or `api`, indicates who added the fact to the DB'
used:
type: boolean
description: 'Weather or not the `Fact` has been sent by the CatBot. This value is reset each time every `Fact` is used'
type:
type: string
description: 'Type of animal the `Fact` describes (e.g. ‘cat’, ‘dog’, ‘horse’)'
This schema describe the endpoint and the model of the CatFact API.
Jane configuration¶
We need to configure Jane before generation. So we create a .jane-openapi
file:
<?php
return [
'openapi-file' => __DIR__ . '/schema.yaml',
'namespace' => 'CatFacts\Api',
'directory' => __DIR__ . '/generated/',
'date-format' => \DateTimeInterface::RFC3339_EXTENDED, // date-time format use by CatFact API
'client' => 'psr18',
];
It will contains a reference to your main schema file (that file can be linked to other files if you want), the PHP namespace you want for generated classes and the directory you want to use.
Warning
Client argument is recommended, if you don’t fill it, it will generated a HTTPlug Client which is not what we are showing here and HTTPlug Client generation is deprecated since Jane v5.1.0
Jane generation¶
Now we can run generation, basically just require jane with composer (see OpenAPI for more details about installation) and run the following command:
vendor/bin/jane-openapi generate
It will find any .jane-openapi
file and use it as configuration. If your file has a different name, just add
-c path/to/my/file
after the command.
Creating a client¶
Then you need a Client to bridge between Jane and your application. Jane use PSR18 to make this bridge easier which allow us to have any middleware we need. If you provide server URL in your schema and you have no authentification needed for your API, then everything will be automated by Jane, you just have to do:
use CatFacts\Api\Client;
$client = Client::create();
And that’s all you need. If you need authentification, please read Using a generated client.
Using your client¶
Finally we can use our Client and try to get some cool cat fact:
<?php
require_once __DIR__.'/vendor/autoload.php';
$client = \CatFacts\ClientFactory::create();
$fact = $client->randomFact();
And this will give us a Fact
object as following:
object(CatFacts\Api\Model\Fact)#29 (10) {
["id":protected]=>
string(24) "591f98108dec2e14e3c20b0f"
["v":protected]=>
int(0)
["user":protected]=>
NULL
["text":protected]=>
string(63) "Cats have been domesticated for half as long as dogs have been."
["updatedAt":protected]=>
object(DateTime)#28 (3) {
["date"]=>
string(26) "2019-08-24 20:20:02.145000"
["timezone_type"]=>
int(2)
["timezone"]=>
string(1) "Z"
}
["sendDate":protected]=>
NULL
["deleted":protected]=>
bool(false)
["source":protected]=>
string(3) "api"
["used":protected]=>
bool(false)
["type":protected]=>
string(3) "cat"
}
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:
<?php
namespace MyApi\Endpoint;
use MyApiGenerated\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:
<?php
namespace MyApi;
use MyApiGenerated\Client as BaseClient;
use MyApi\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.
AutoMapper¶
Jane AutoMapper is an experimental library that generate automapper class which allows to automap values from Class to Class.
WIP