Design, create and test your API in Symfony

API Blueprint

A powerful high-level API description language for web APIs.

API Blueprint

Blueprint - format

FORMAT: 1A
HOST: https://erp.local/api

# ERP API Documentation
REST API documentation for ERP application

## API Authorization
User authorization is based on JWT: [https://jwt.io/](https://jwt.io/)

Header example:
```http
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9
```

## General responses
Api response that can occur on each request

**401 Unauthorized**

User not authorized with correct token

Example response:
```http
{
  "error": {
    "code": 401,
    "message": "Unauthorized"
  }
}
```

API - data structures

# Data Structures

## EmployeeData
+ id: 1 (required, number) - Unique identifier
+ firstName: Jan (required, string)
+ lastName: Kowalski (required, string)
+ legalBasis: contract (required, string)
+ position: accountant (required, string)
+ status: hired (required, string)

## EmployeeList
+ page: 1 (number)
+ limit: 10 (number)
+ pages: 1 (number)
+ total: 2 (number)
+ _links
    + self
        + href: "/api/employee/?page=1&limit=10"
    + first
        + href: "/api/employee/?page=1&limit=10"
    + last
        + href: "/api/employee/?page=1&limit=10"
+ _embedded
    + items (array)
        + (EmployeeData)
        + (EmployeeData)

API - groups

# Group Employee

## List [/employee]

### Get employees [GET]
Get a list of employees.

+ Response 200 (application/json)

    + Attributes (EmployeeList)

### Create new employee [POST]
Create a new employee from request data

+ Request (application/json)

    + Body

            {
                "firstName": "John",
                "lastName": "Doe",
                "legalBasis": "contract",
                "position": "accountant",
                "status": "hired"
            }

+ Response 201

    + Attributes (EmployeeData)

+ Response 400 (application/json)

    + Body

            {
                "error": "Invalid title"
            }

API - schema

### Suggest Employee [GET]
Get employee suggestions based on text input

+ Response 200 (application/json)

    + Attributes (array[EmployeeSuggestData])

    + Schema

            {
                "$schema": "http://json-schema.org/draft-04/schema#",
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "first_name": {
                            "type": "string"
                        },
                        "last_name": {
                            "type": "string"
                        }
                    }
                }
            }

Aglio

An API Blueprint renderer that supports multiple themes and outputs static HTML that can be served by any web host.

Aglio

Aglio

aglio --theme-variables slate 
    -i app/Resources/blueprints/api.apib 
    -o web/apidoc.html
tools/apidoc.sh

Aglio

Dredd

Dredd is a language-agnostic command-line tool for validating API documentation written in API Blueprint format against its backend implementation.

Frontend testuje backend !!!

Dredd - config

dry-run: null
hookfiles:
  - tests/dredd/hooks.php
language: vendor/ddelnano/dredd-hooks-php/bin/dredd-hooks-php
sandbox: false
server: null
server-wait: 3
init: false
custom: {}
names: false
only: []
reporter: []
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
blueprint: app/Resources/blueprints/api.apib
endpoint: 'http://localhost/api'

Dredd - hooks

<?php
use Dredd\Hooks;

$loader = require __DIR__.'/../../app/autoload.php';
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel->boot();
$container = $kernel->getContainer();

$user = $container->get('dvs.user_bundle.repository.doctrine.user_repository')->findByUsername('admin');
$tokenManager = $container->get('lexik_jwt_authentication.jwt_manager');
$token = $tokenManager->create($user);

Hooks::beforeEach(function(&$transaction) use ($token) {
    $transaction->request->
        headers->Authorization = 'Bearer '.$token;
});

Dredd - in action

dredd

tools/test_api.sh

SyliusResourceBundle

Rapid RESTful API Development

SyliusResourceBundle

SyliusResourceBundle

sylius_resource:
    resources:
        dvs.employee:
            classes:
                model: Dvs\Domain\Employee\Entity\Employee
<?php

declare (strict_types = 1);

namespace Dvs\Domain\Employee\Entity;

use Sylius\Component\Resource\Model\ResourceInterface;

class Employee implements ResourceInterface
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $firstName;

    /**
     * @var string
     */
    private $lastName;

    /**
     * @var string
     */
    private $legalBasis;

    /**
     * @var string
     */
    private $position;

    /**
     * @var string
     */
    private $status;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getFirstName()
    {
        return $this->firstName;
    }

    /**
     * @param string $firstName
     */
    public function setFirstName(string $firstName)
    {
        $this->firstName = $firstName;
    }

    /**
     * @return string
     */
    public function getLastName()
    {
        return $this->lastName;
    }

    /**
     * @param string $lastName
     */
    public function setLastName(string $lastName)
    {
        $this->lastName = $lastName;
    }

    /**
     * @return string
     */
    public function getLegalBasis()
    {
        return $this->legalBasis;
    }

    /**
     * @param string $legalBasis
     */
    public function setLegalBasis(string $legalBasis)
    {
        $this->legalBasis = $legalBasis;
    }

    /**
     * @return string
     */
    public function getPosition()
    {
        return $this->position;
    }

    /**
     * @param string $position
     */
    public function setPosition(string $position)
    {
        $this->position = $position;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param string $status
     */
    public function setStatus(string $status)
    {
        $this->status = $status;
    }

    /**
     * @return array
     */
    public function getSuggest()
    {
        $name = sprintf('%s %s', $this->firstName, $this->lastName);
        $reversedName = sprintf('%s %s', $this->lastName, $this->firstName);
        $input = [$this->firstName, $this->lastName, $name, $reversedName];
        $payload = ['first_name' => $this->firstName, 'last_name' => $this->lastName];

        return [
            'input' => $input,
            'output' => $name,
            'payload' => $payload,
        ];
    }
}

SRB - routing

dvs_employee:
    resource: |
        alias: dvs.employee
        path: employee
        except: ['delete']
    type: sylius.resource_api
    defaults:
        _sylius:
            sortable: true
            filterable: true
app/console debug:router
...
dvs_employee_index            GET         ANY      ANY    /api/employee                           
dvs_employee_create           POST        ANY      ANY    /api/employee                           
dvs_employee_update           PUT|PATCH   ANY      ANY    /api/employee/{id}                      
dvs_employee_show             GET         ANY      ANY    /api/employee/{id}           

SRB - validation

<?php


class UpdateNotificationSettingsType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('daysBeforeExpiration', 'number', [
                'constraints' => [
                    new NotBlank(),
                    new Range([
                        'min' => 0,
                        'max' => 180,
                    ]),
                ],
            ])
            ->add('method', 'choice', [
                'constraints' => [
                    new NotBlank(),
                ],
                'choices' => NotificationType::getChoices(),
            ]);
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'updateNotificationSettingsType';
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">

    <class name="Dvs\Domain\Employee\Entity\Employee">
        <property name="firstName">
            <constraint name="NotBlank"/>
        </property>
        <property name="lastName">
            <constraint name="NotBlank"/>
        </property>
        <property name="legalBasis">
            <constraint name="NotBlank"/>
            <constraint name="Choice">
                <option name="callback">
                    <value>Dvs\Domain\Employee\Type\LegalBasisType</value>
                    <value>getValues</value>
                </option>
            </constraint>
        </property>
        <property name="position">
            <constraint name="NotBlank"/>
            <constraint name="Choice">
                <option name="callback">
                    <value>Dvs\Domain\Employee\Type\PositionType</value>
                    <value>getValues</value>
                </option>
            </constraint>
        </property>
        <property name="status">
            <constraint name="NotBlank"/>
            <constraint name="Choice">
                <option name="callback">
                    <value>Dvs\Domain\Employee\Type\StatusType</value>
                    <value>getValues</value>
                </option>
            </constraint>
        </property>
    </class>
</constraint-mapping>

SRB - list

{
  "page": 1,
  "limit": 10,
  "pages": 6,
  "total": 52,
  "_links": {
    "self": {
      "href": "/api/employee?page=1&limit=10"
    },
    "first": {
      "href": "/api/employee?page=1&limit=10"
    },
    "last": {
      "href": "/api/employee?page=6&limit=10"
    },
    "next": {
      "href": "/api/employee?page=2&limit=10"
    }
  },
  "_embedded": {
    "items": [
      {
        "id": 3,
        "firstName": "Dariusz",
        "lastName": "Szymczak",
        "legalBasis": "contract",
        "position": "fitter",
        "status": "candidate"
      },
      {
        "id": 19,
        "firstName": "Anita",
        "lastName": "Król",
        "legalBasis": "order",
        "position": "accountant",
        "status": "hired"
      },
      {
        "id": 27,
        "firstName": "Arkadiusz",
        "lastName": "Tomaszewski",
        "legalBasis": "none",
        "position": "accountant",
        "status": "dismissed"
      },
      {
        "id": 32,
        "firstName": "Dominik",
        "lastName": "Grabowska",
        "legalBasis": "order",
        "position": "staff",
        "status": "hired"
      },
      {
        "id": 35,
        "firstName": "Dariusz",
        "lastName": "Dudek",
        "legalBasis": "contract",
        "position": "fitter",
        "status": "dismissed"
      },
      {
        "id": 36,
        "firstName": "Alicja",
        "lastName": "Brzezińska",
        "legalBasis": "contract",
        "position": "management-assistant",
        "status": "dismissed"
      },
      {
        "id": 37,
        "firstName": "Dawid",
        "lastName": "Krawczyk",
        "legalBasis": "order",
        "position": "foreman",
        "status": "hired"
      },
      {
        "id": 40,
        "firstName": "Apolonia",
        "lastName": "Zakrzewska",
        "legalBasis": "recruitment",
        "position": "foreman",
        "status": "dismissed"
      },
      {
        "id": 42,
        "firstName": "Anita",
        "lastName": "Borowski",
        "legalBasis": "contract",
        "position": "fitter",
        "status": "hired"
      },
      {
        "id": 50,
        "firstName": "Bianka",
        "lastName": "Michalak",
        "legalBasis": "recruitment",
        "position": "fitter",
        "status": "dismissed"
      }
    ]
  }
}

SRB - sort & filter

GET /api/employee?sorting[firstName]=asc
GET /api/employee?criteria[position]=fitter
&criteria[status]=dismissed

SRB - want more ?

http://docs.sylius.org/en/latest/
bundles/SyliusResourceBundle/

Thx !

Your are the best team I've worked with

Design, document, create and test your API in Symfony

By Arkadiusz Kondas

Design, document, create and test your API in Symfony

Symfony + SyliusResourceBundle + API Blueprint + Aglio + Dredd = Perfect API

  • 1,331