OpenAPI Generator - use Symfony Bundle in your Project
Generate Symfony Bundle from OpenAPI Spec and use it in your Symfony 4 Projects.
First, we set up a new Symfony 4 Project with the Symfony CLI, if you don't already have this on your machine go here to use the installer to make it available on the CLI.
symfony new petstore-symfony --version=4.4 --full
We need a few additional packages and install them via Composer.
composer require ext-curl ext-json ext-mbstring jms/serializer-bundle
We create a new folder in the root of the Symfony Project and add there our config for generating/regenerating the OpenApiBundle at any time, so we can easily keep it in sync with the changes we make to the OpenAPI Spec.
mkdir openapi
In this folder, we will add two files config.json and openapi-generator.php
In config.json we can set the additional-properties, which you can review here. We only use the property invokerPackage to change the Namespace to App\OpenApiBundle.
// config.json
{
"invokerPackage": "App\\OpenApiBundle"
}
Install the openapi-generator-cli via npm command, so we can easily generate code from the command line.
You need to have Node.js installed on your machine to have npm available via the command line.
npm i @openapitools/openapi-generator-cli
In openapi-generator.php we will add some logic for generating the OpenApiBundle from the OpenAPI Spec, in this example, we will use the pet store spec.
<?php// OpenAPI Specification
$urlToOpenApiYaml = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml";
// the generated code will be added to src/OpenApiBundle
$outputPath = "src/OpenApiBundle";// npm command to generate code from the spec
$output = shell_exec("openapi-generator-cli generate -g php-symfony -i $urlToOpenApiYaml -o $outputPath -c openapi/config.json");
echo "$output";// remove some generated code to concentrate on the main parts
shell_exec("rm -rf $outputPath/Tests");
shell_exec("rm -rf $outputPath/src");
shell_exec("rm $outputPath/.coveralls.yml");
shell_exec("rm $outputPath/.gitignore");
shell_exec("rm $outputPath/.openapi-generator-ignore");
shell_exec("rm $outputPath/.php_cs.dist");
shell_exec("rm $outputPath/.travis.yml");
shell_exec("rm $outputPath/autoload.php");
shell_exec("rm $outputPath/composer.json");
shell_exec("rm $outputPath/git_push.sh");
shell_exec("rm $outputPath/phpunit.xml.dist");
shell_exec("rm $outputPath/pom.xml");
Add a new command in the composer.json in the scripts section.
"scripts": {
...
"generate-openapi-bundle": "php openapi/openapi-generator.php"
},
Now we can run the code generation easily via:
composer run generate-openapi-bundle
You should see some successful log info on the command line that lists the generated files.
Now you have a new folder under src/ the OpenApiBundle. If you worked before with Symfony you know most of the folders. Here is a short explanation of what we have:
Now we do some preparation before implementing our services.
In config/bundles.php we add our generated Bundle to the list, to load it into our project.
return [
...
App\OpenApiBundle\OpenAPIServerBundle::class => ['all' => true],
];
In config/routes.yaml we include the routes from your generated bundle.
OpenAPIBundle:
resource: "@OpenAPIServerBundle/Resources/config/routing.yml"
prefix: /
We delete the src/Contoller folder and remove the following part from config/routes/annotations.yaml
controllers:
resource: ../../src/Controller/
type: annotation
And we also remove this part from config/services.yaml
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
For this example I will use a very simple folder structure, you can change it easily to your needs.
Let's start to implement our API. We add a new file into the folder src/Api/
We extend the Controller from our generated Bundle so we have some logic out of the box and implement the Interface PetsApiInterface that verifies that we implement all needed functions with the correct parameters.
// src/Api/PetsApi.php
<?php
namespace App\Api;
use App\OpenApiBundle\Api\PetsApiInterface;
use App\OpenApiBundle\Controller\Controller;
class PetsApi extends Controller implements PetsApiInterface
{
public function createPets(&$responseCode, array &$responseHeaders)
{
// TODO: Implement createPets() method.
}
public function listPets($limit = null, &$responseCode, array &$responseHeaders)
{
// TODO: Implement listPets() method.
}
public function showPetById($petId, &$responseCode, array &$responseHeaders)
{
// TODO: Implement showPetById() method.
}
}
Before we move on we will configure the connection to the database. Please replace in the .env file the Parameter DATABASE_URL with your connection details. I will use the name petstore for the database.
You find more about the Database and the Doctrine ORM here.
// replace with your config
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
We will generate the database from the command line with:
php bin/console doctrine:database:create
We generate a new entity with the same fields [name, tag] as src/OpenApiBundle/Model/Pet.php and name it PetEntity
php bin/console make:entity
With the next command, we will sync our new Entity with our Database.
php bin/console doctrine:migrations:migrate
Now, we start to implement our endpoints.
To our PetEntityRepository file, we will add three methods to create a pet, list all pets and find one pet by id.
// src/Repository/PetEntityRepository.php
<?php
namespace App\Repository;
use App\Entity\PetEntity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PetEntityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PetEntity::class);
}
public function create()
{
$entityManager = $this->getEntityManager();
$pet = new PetEntity();
$pet->setName('Buddy');
$pet->setTag('Dog');
$entityManager->persist($pet);
$entityManager->flush();
return $pet;
}
public function list($limit)
{
return $this->createQueryBuilder('p')
->orderBy('p.name', 'ASC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function findById($id)
{
return $this->find($id);
}
}
Fill the TODOs in PetsApi functions with calls to our repository.
// src/Api/PetsApi.php
<?php
namespace App\Api;
use App\Entity\PetEntity;
use App\OpenApiBundle\Api\PetsApiInterface;
use App\OpenApiBundle\Controller\Controller;
class PetsApi extends Controller implements PetsApiInterface
{
public function createPets(&$responseCode, array &$responseHeaders)
{
return $this->getDoctrine()
->getRepository(PetEntity::class)
->create();
}
public function listPets($limit = null, &$responseCode, array &$responseHeaders)
{
return $this->getDoctrine()
->getRepository(PetEntity::class)
->list($limit);
}
public function showPetById($petId, &$responseCode, array &$responseHeaders)
{
return $this->getDoctrine()
->getRepository(PetEntity::class)
->findById($petId);
}
}
Now we need to tell Symfony which service should be used for our pet's endpoints config/services.yaml.
petstore.api.client:
class: App\Api\PetsApi
tags:
- { name: "open_api_server.api", api: "pets" }
When you implement your own services and want to know which tags you need to use you can check the automatically generated docs in this folder src/OpenApiBundle/Resources/docs/Api/…
We can start a local server with the following command.
symfony server:start
We can check the available routes with:
php bin/console debug:router
Let's send a few POST requests to the following URL to generate some dummy entries in the DB, via Postman our your preferred environment. We can pass the optional query parameter limit to get a chunk of data.
http://127.0.0.1:8000/pets
Afterward, we can get a list of all generated pets.
http://127.0.0.1:8000/pets
We can also get a single entry via the following URL.
http://127.0.0.1:8000/pets/1
If you want to dig deeper into how your OpenAPI Spec is related to the generated code, here are a few hints.
At the time of writing the OpenAPI Generator for Symfony 5 is in development, I will write an updated article a soon as it is available.
You find the final solution to this article here.
I hope you enjoyed this article. Your questions and feedback are very welcome.