đ± Un ensemble de librairies pour gĂ©nĂ©rer des modĂšles et clients API basĂ©s sur les spĂ©cifications JSON Schema et OpenAPI
{
"type": "object",
"properties": {
"name": { "type": "string" },
"brewer": { "type": "string" },
"style": { "type": "string" },
"volume": { "type": "integer" },
"alcohol": { "type": "integer" },
"country": { "type": "string" },
"color": { "type": "string" }
}
}
{
"type": "object",
"properties": {
"name": { "type": "string" },
"brewer": { "type": "string" },
"style": { "type": "string" },
"volume": {
"type": "integer",
"minimum": 0
},
"alcohol": { "type": "integer" },
"country": { "type": "string" },
"color": { "type": "string" }
}
}
{
"type": "object",
"properties": {
"names": {
"type": "array",
"items": {
"type": "string"
}
},
"brewer": { "type": "string" },
"style": { "type": "string" },
"volume": { "type": "integer" },
"alcohol": { "type": "integer" },
"country": { "type": "string" },
"color": { "type": "string" }
}
}
{
"definitions": {
"Beer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"brewer": { "$ref": "#/definitions/Brewer" },
"style": { "type": "string" },
"volume": { "type": "integer" },
"alcohol": { "type": "integer" },
"color": { "type": "string" }
}
},
"Brewer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"country": { "type": "string" }
}
}
}
}
project/config/jane/json-schema.json
{
"type": "object",
"properties": {
"name": { "type": "string" },
"brewer": { "type": "string" },
"style": { "type": "string" },
"volume": { "type": "integer" },
"alcohol": { "type": "integer" },
"country": { "type": "string" },
"color": { "type": "string" }
}
}
project/config/jane/json_schema.php
return [
'json-schema-file' => __DIR__ . '/json-schema.json',
'root-class' => 'Beer',
'namespace' => 'Generated',
'directory' => __DIR__ . '/../../generated',
];
vendor/bin/jane generate -c config/jane/json_schema.php
class Beer
{
/**
*
*
* @var string
*/
protected $name;
/**
*
*
* @var string
*/
protected $brewer;
/**
*
*
* @var string
*/
protected $style;
/**
*
*
* @var int
*/
protected $volume;
/**
*
*
* @var int
*/
protected $alcohol;
/**
*
*
* @var string
*/
protected $country;
/**
*
*
* @var string
*/
protected $color;
/**
*
*
* @return string
*/
public function getName() : string
{
return $this->name;
}
/**
*
*
* @param string $name
*
* @return self
*/
public function setName(string $name) : self
{
$this->name = $name;
return $this;
}
/**
*
*
* @return string
*/
public function getBrewer() : string
{
return $this->brewer;
}
/**
*
*
* @param string $brewer
*
* @return self
*/
public function setBrewer(string $brewer) : self
{
$this->brewer = $brewer;
return $this;
}
/**
*
*
* @return string
*/
public function getStyle() : string
{
return $this->style;
}
/**
*
*
* @param string $style
*
* @return self
*/
public function setStyle(string $style) : self
{
$this->style = $style;
return $this;
}
/**
*
*
* @return int
*/
public function getVolume() : int
{
return $this->volume;
}
/**
*
*
* @param int $volume
*
* @return self
*/
public function setVolume(int $volume) : self
{
$this->volume = $volume;
return $this;
}
/**
*
*
* @return int
*/
public function getAlcohol() : int
{
return $this->alcohol;
}
/**
*
*
* @param int $alcohol
*
* @return self
*/
public function setAlcohol(int $alcohol) : self
{
$this->alcohol = $alcohol;
return $this;
}
/**
*
*
* @return string
*/
public function getCountry() : string
{
return $this->country;
}
/**
*
*
* @param string $country
*
* @return self
*/
public function setCountry(string $country) : self
{
$this->country = $country;
return $this;
}
/**
*
*
* @return string
*/
public function getColor() : string
{
return $this->color;
}
/**
*
*
* @param string $color
*
* @return self
*/
public function setColor(string $color) : self
{
$this->color = $color;
return $this;
}
}
class BeerNormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface
{
use DenormalizerAwareTrait;
use NormalizerAwareTrait;
use CheckArray;
public function supportsDenormalization($data, $type, $format = null)
{
return $type === 'Generated\\Model\\Beer';
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \Generated\Model\Beer;
}
public function denormalize($data, $class, $format = null, array $context = array())
{
if (isset($data['$ref'])) {
return new Reference($data['$ref'], $context['document-origin']);
}
if (isset($data['$recursiveRef'])) {
return new Reference($data['$recursiveRef'], $context['document-origin']);
}
$object = new \Generated\Model\Beer();
if (\array_key_exists('name', $data)) {
$object->setName($data['name']);
}
if (\array_key_exists('brewer', $data)) {
$object->setBrewer($data['brewer']);
}
if (\array_key_exists('style', $data)) {
$object->setStyle($data['style']);
}
if (\array_key_exists('volume', $data)) {
$object->setVolume($data['volume']);
}
if (\array_key_exists('alcohol', $data)) {
$object->setAlcohol($data['alcohol']);
}
if (\array_key_exists('country', $data)) {
$object->setCountry($data['country']);
}
if (\array_key_exists('color', $data)) {
$object->setColor($data['color']);
}
return $object;
}
public function normalize($object, $format = null, array $context = array())
{
$data = array();
if (null !== $object->getName()) {
$data['name'] = $object->getName();
}
if (null !== $object->getBrewer()) {
$data['brewer'] = $object->getBrewer();
}
if (null !== $object->getStyle()) {
$data['style'] = $object->getStyle();
}
if (null !== $object->getVolume()) {
$data['volume'] = $object->getVolume();
}
if (null !== $object->getAlcohol()) {
$data['alcohol'] = $object->getAlcohol();
}
if (null !== $object->getCountry()) {
$data['country'] = $object->getCountry();
}
if (null !== $object->getColor()) {
$data['color'] = $object->getColor();
}
return $data;
}
}
project/src/Command/IndexCommand.php
// On va récupérer l'index Elasticsearch
$beers = $this->beerRepository->findAll();
foreach ($beers as $beer) {
$model = new \Generated\Beer();
$model->setName($beer->getName());
$model->setBrewer($beer->getBrewer());
$model->setStyle($beer->getStyle());
$model->setVolume($beer->getVolume());
$model->setAlcohol($beer->getAlcohol());
$model->setCountry($beer->getCountry());
$model->setColor($beer->getColor());
$document = new \Elastica\Document($beer->getId(), $model);
$indexer->scheduleIndex(self::BEERS_INDEX, $document);
}
// Puis on envoit les documents Ă Elasticsearch
final class Mapper_App_Entity_Beer_Generated_Model_Beer extends \Jane\AutoMapper\Mapper
{
protected $hash = '15937785381593778538';
public function __construct()
{
}
public function &map($value, \Jane\AutoMapper\Context $context)
{
if (null === $value) {
return $value;
}
$result = $context->getObjectToPopulate();
if (null === $result) {
$result = new \Generated\Model\Beer();
}
if ($context->isAllowedAttribute('style')) {
$result->setstyle($value->getstyle());
}
if ($context->isAllowedAttribute('volume')) {
$result->setvolume($value->getvolume());
}
if ($context->isAllowedAttribute('alcohol')) {
$result->setalcohol($value->getalcohol());
}
if ($context->isAllowedAttribute('name')) {
$result->setname($value->getname());
}
if ($context->isAllowedAttribute('brewer')) {
$result->setbrewer($value->getbrewer());
}
if ($context->isAllowedAttribute('country')) {
$result->setcountry($value->getcountry());
}
if ($context->isAllowedAttribute('color')) {
$result->setcolor($value->getcolor());
}
return $result;
}
public function injectMappers(\Jane\AutoMapper\AutoMapperInterface $autoMapper)
{
}
}
class User
{
private int $id;
public string $name;
public int $age;
public function __construct(int $id, string $name, int $age)
{
$this->id = $id;
$this->name = $name;
$this->age = $age;
}
public function getId(): int
{
return $this->id;
}
}
class UserName
{
public string $name;
}
$source = [
'id' => 42,
'name' => 'Bastien',
'age' => 30,
];
$target = $automapper->map($source, User::class);
// same as đ
$target = new UserName();
$target = $automapper->map($source, $target);
$source = new User(42, 'Bastien', 30);
$target = $automapper->map($source, 'array');
$source = new User(42, 'Bastien', 30);
$target = $automapper->map($source, UserName::class);
project/src/Command/IndexCommand.php
// On va récupérer l'index Elasticsearch
$beers = $this->beerRepository->findAll();
foreach ($beers as $beer) {
$model = $this->autoMapper->map($beer, \Generated\Beer::class);
$document = new \Elastica\Document($beer->getId(), $model);
$indexer->scheduleIndex(self::BEERS_INDEX, $document);
}
// Puis on envoie les documents Ă Elasticsearch
project/src/Controller/BeerController.php
// On récupÚre tous les Document de notre index
$beers = [];
foreach ($results as $result) {
$beers[] = $result->getModel();
}
return $this->json($beers);
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
securitySchemes:
BasicAuth:
type: http
scheme: basic
BearerAuth:
type: http
scheme: bearer
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
OpenID:
type: openIdConnect
openIdConnectUrl: https://example.com/.well-known/openid-configuration
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes:
read: Grants read access
write: Grants write access
admin: Grants access to admin operations
project/config/jane/open-api.yaml
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â)'
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 }
__v: { type: integer }
user: { type: string }
text: { type: string }
updatedAt: { type: string, format: date-time }
sendDate: { type: string }
deleted: { type: boolean }
source: { type: string }
used: { type: boolean }
type: { type: string }
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:
text: { type: string }
updatedAt: { type: string, format: date-time }
project/config/jane/open_api.php
return [
'openapi-file' => __DIR__ . '/open-api.yaml',
'namespace' => 'CatFacts\Api',
'directory' => __DIR__ . '/../../generated',
'date-format' => \DateTimeInterface::RFC3339_EXTENDED,
];
project/config/packages/jane.yaml
services:
_defaults:
autowire: true
autoconfigure: true
CatFacts\Api\Normalizer\JaneObjectNormalizer: ~
CatFacts\Api\Client:
factory: ['CatFacts\Api\Client', 'create']
project/src/Controller/FactController.php
class FactController extends AbstractController
{
public function index(CatFacts\Api\Client $client)
{
return $this->render('fact.html.twig', [
'fact' => $client->randomFact(),
]);
}
}