Home

Awesome

yii2-openapi

REST API application generator for Yii2, openapi 3.0 YAML -> Yii2.

Base on Gii, the Yii Framework Code Generator.

Latest Stable Version Latest Alpha Version Total Downloads License yii2-openapi

TLDR; what is this?

A code generator for OpenAPI and Yii Framework based PHP API application.

Input: OpenAPI 3.0 YAML or JSON (via cebe/php-openapi)

Output: Yii Framework Application with Controllers, Models, database schema

Features

Currently available features:

Requirements

Install

composer require php-openapi/yii2-openapi:^2.0@beta

Usage

You can use this package in your existing application or start a new project using the yii2-app-api application template. For usage of the template, see instructions in the template repo readme.

In your existing Yii application config (works for console as well as web):

<?php
$config = [
    // ... this is your application config ...
];

if (YII_ENV_DEV) {
    // enable Gii module
    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = [
        'class' => \yii\gii\Module::class,
        'generators' => [
            // add ApiGenerator to Gii module
            'api' => \cebe\yii2openapi\generator\ApiGenerator::class,

            // --------- OR ---------
            // to disable generation of migrations files or with default config change
            'api' => [
              'class' => \cebe\yii2openapi\generator\ApiGenerator::class,
              'generateMigrations' => false, # this config can also be applied in CLI command
            ],
        ],
    ];
}

return $config;

To use the web generator, open index.php?r=gii and select the REST API Generator.

On console, you can run the generator with ./yii gii/api --openApiPath=@app/openapi.yaml. Where @app/openapi.yaml should be the absolute path to your OpenAPI spec file. This can be JSON as well as YAML (see also php-openapi/php-openapi for supported formats).

Run ./yii gii/api --help for all options. Example: Disable generation of migrations files ./yii gii/api --generateMigrations=0

See Petstore example for example OpenAPI spec.

OpenAPI extensions

This library understands the following extensions to the OpenAPI spec:

x-faker

You may specify custom PHP code for generating fake data for a property:

    Post:
      properties:
        id:
          type: integer
        tags:
          type: array
          items:
            type: string
          example: ['one', 'two']
          x-faker: "$faker->randomElements(['one', 'two', 'three', 'four'])"

To avoid generating faker code for particular model attribute, use value false:

    Post:
      properties:
        age:
          type: integer
          x-faker: false

Using in reference with allOf:

    Invoice:
      type: object
      required:
        - id
      properties:
        id:
          type: integer

    Order:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        invoice:
          allOf:
            - $ref: '#/components/schemas/Invoice'
            - x-faker: false

x-table

Specify the table name for a Schema that defines a model which is stored in the database. You can generate non-db model based on \yii\base\Model without migrations by setting x-table: false

x-pk

Explicitly specify primary key name for table, if it is different from "id"

    Post:
      x-table: posts
      x-pk: uid
      properties:
        uid:
           type: integer
        title:
           type: string

x-db-type

Explicitly specify the database type for a column. (MUST contain only real DB type! (json, jsonb, uuid, varchar etc.)). If x-db-type is set to false, property will be processed as virtual; It will be added in model as public property, but skipped for migrations generation.

Example values of x-db-type are:

Such values are not allowed:

If enum and x-db-type both are provided then for database column schema (migrations), only x-db-type will be considered ignoring enum.

x-indexes

Specify table indexes

    Post:
      x-table: posts
      x-indexes:
          - 'visible,publish_date'
          - 'unique:title' #for unique attributes also unique validation check will be added
          - 'gist:metadata' #for postgres will generate index using GIST index type
      properties:
        id:
           type: integer
           x-db-type: INTEGER PRIMARY KEY AUTO_INCREMENT
        title:
           type: string
        visible:
            type: boolean
        publish_date:
            type: string
            format: date
        metadata:
           type: object
           x-db-type: JSON
           default: '{}' 

If raw DB expression is needed in index, then it must be for only one column. Example:

    x-indexes:
       - "gin(to_tsvector('english', search::text)):search" # valid
       - "gin(to_tsvector('english', search::text)):search,column2" # invalid

x-db-default-expression

Ability to provide default value by database expression

created_at:
  readOnly: true
  type: string
  format: datetime
  x-db-type: datetime
  nullable: false
  x-db-default-expression: current_timestamp()

Note: If both default and x-db-default-expression are present then default will be considered.

created_at:
  readOnly: true
  type: string
  format: datetime
  x-db-type: datetime
  nullable: false
  x-db-default-expression: current_timestamp() # this will be ignored
  default: "2011-11-11" # this will be considered

Also see: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html

x-fk-on-delete

Allow to set foreign key constraint in migrations for ON DELETE event of row in database table. Example:

  components:
    schemas:
      User:
        type: object
        description: x on-x (update|delete) foreign key constraint
        properties:
          id:
            type: integer
          name:
            type: string
      Post:
        type: object
        description: x on-x (update|delete) foreign key constraint
        properties:
          id:
            type: integer
          title:
            type: string
          user:
            allOf:
              - $ref: '#/components/schemas/User'
              - x-fk-on-update: CASCADE
          user_2:
            allOf:
              - $ref: '#/components/schemas/User'
              - x-fk-on-update: CASCADE
              - x-fk-on-delete: SET NULL
          user_3:
            allOf:
              - $ref: '#/components/schemas/User'
              - x-fk-on-delete: SET NULL
          user_4:
            $ref: '#/components/schemas/User' # without any constraints

x-fk-on-update

Allow to set foreign key constraint in migrations for ON UPDATE event of row in database table. For example, see above section for x-fk-on-delete.

x-fk-column-name

Provide custom database table column name in case of relationship column. This will not reflect in models relations, faker etc. Example:

  components:
    schemas:
      Webhook:
        type: object
        description: example for x-fk-column-name
        properties:
          id:
            type: integer
          name:
            type: string
          user:
            $ref: '../openapi.yaml#/components/schemas/User' # this will automatically create `user_id` column
          redelivery_of:
            allOf:
              - $ref: '../openapi.yaml#/components/schemas/Delivery'
              # this will automatically create `redelivery_of_id` column, but to avoid that use below extension
              - x-fk-column-name: redelivery_of # this will create `redelivery_of` column instead of `redelivery_of_id`

x-deleted-schemas

This is root level key used to generate "drop table" migration for the deleted component schema. If a component schema (DB model) is removed from OpenAPI spec then its following entities should be also deleted from the code:

So to generate appropriate migration for the removed schema, explicitly setting schema name or schema name + custom table name is required in this key. Only then the migrations will be generated. It should be set as:

x-deleted-schemas:
  - Fruit # Example: table name is evaluated to `itt_fruits`, if `itt_` is prefix set in DB config
  - Mango: the_mango_table_name # custom table name; see `x-table` in README.md

x-no-relation

To differentiate a component schema property from one-to-many or many-to-many relation in favour of array(json) of related objects, x-no-relation (type: boolean, default: false) is used.

        comments:
          type: array
          items:
            $ref: "#/components/schemas/Comment"

This will not generate 'comments' column in database migrations. But it will generate getComments() relation in Yii model file.

In order to make it real database column, extension x-no-relation can be used.

        comments:
          type: array
          x-no-relation: true
          items:
            $ref: "#/components/schemas/Comment"

Database column type can be array, json etc. to store such data.

Now if the Comment schema from the above example is

    Comment:
      properties:
        id:
          type: integer
        content:
          type: string

then the value for comments can be

[
  {
    "id": 1,
    "content": "Hi there"
  },
  {
    "id": 2,
    "content": "Hi there 2"
  }
]

x-no-relation can be only used with OpenAPI schema data type array.

x-route

To customize route (controller ID/action ID) for a path, use custom key x-route with value <controller ID>/<action ID>. It can be used for non-crud paths. It must be used under HTTP method key but not directly under the paths key of OpenAPI spec. Example:

paths:
  /payments/invoice/{invoice}:
    parameters:
      - name: invoice
        in: path
        description: lorem ipsum
        required: true
        schema:
          type: integer
    post:
      x-route: 'payments/invoice'
      summary: Pay Invoice
      description: Pay for Invoice with given invoice number
      requestBody:
        description: Record new payment for an invoice
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Payments'
        required: true
      responses:
        '200':
          description: Successfully paid the invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'

It won't generate actionCreateInvoice in PaymentsController.php file, but will generate actionInvoice instead in same file.

Generated URL rules config for above is (in urls.rest.php or pertinent file):

    'POST payments/invoice/<invoice:\d+>' => 'payments/invoice',
    'payments/invoice/<invoice:\d+>' => 'payments/options',

Also, if same action is needed for HTTP GET and POST then use same value for x-route. Example:

paths:
  /a1/b1:
    get:
      x-route: 'abc/xyz'
      operationId: opnid1
      summary: List
      description: Lists
      responses:
        '200':
          description: The Response
    post:
      x-route: 'abc/xyz'
      operationId: opnid2
      summary: create
      description: create
      responses:
        '200':
          description: The Response

Generated URL rules config for above is (in urls.rest.php or pertinent file):

    'GET a1/b1' => 'abc/xyz',
    'POST a1/b1' => 'abc/xyz',
    'a1/b1' => 'abc/options',

x-route does not support Yii Modules.

x-description-is-comment

boolean; default: false

When a new database table is created from new OpenAPI component schema, description of a property will be used as comment of column (of database table).

This extension is used when a description is edited for existing property, and you want to generate migration for its corresponding column comment changes.

This extension can be used at 3 place:

1. root level (highest priority)

openapi: 3.0.3
x-description-is-comment: true
info:
  title: Description

This will create migration of any changed description of component schema property present throughout the spec.

2. component schema level

components:
  schemas:
    Fruit:
      type: object
      x-description-is-comment: true

This will create migration of changed description of only properties of component schema which have this extension.

3. property level (lowest priority)

components:
  schemas:
    Fruit:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          nullable: false
          x-description-is-comment: true
          description: Hi there

Migrations will be only generated for changed description of properties having this extension.

Many-to-Many relation definition

There are two ways for define many-to-many relations:

Simple many-to-many without junction model

Post:
  properties:
  ...
    tags:
      type: array
      items:
        $ref: '#/components/schemas/Tag'

Tag:
  properties:
  ...
    posts:
      type: array
      items:
        $ref: '#/components/schemas/Post'

Many-to-many with junction model

This way allowed creating multiple many-to-many relations between to models

# Model TeamMembers with table team_members will be generated with columns team_id, user_id and role
junction_TeamMembers:
   team:
      $ref: '#/components/schemas/Team'
   user:
      $ref: '#/components/schemas/User'
   role:
     type: string
Team:
  properties:
  ...
     team_members:
       type: array
       items:
         $ref: '#/components/schemas/junction_TeamMembers'

User:
  properties:
  ...
    memberships: #You absolutely free with naming for relationship attributes
      type: array
      items:
        $ref: '#/components/schemas/junction_TeamMembers'

Handling of NOT NULL constraints

NOT NULL in DB migrations is determined by nullable and required properties of the OpenAPI schema. e.g. attribute = 'my_property'.

  ExampleSchema:
    properties:
      my_property:
        type: string
  ExampleSchema:
    required:
     - my_property
    properties:
      my_property:
        type: string
  ExampleSchema:
    required:
      - my_property
    properties:
      my_property:
        type: string
        nullable: true
  test_table:
    required:
    properties:
      my_property:
        type: string
        nullable: false

Handling of enum (#enum)

It works on all 3 DB: MySQL, MariaDb and PgSQL.

 test_table:
   properties:
     my_property:
       enum:
         - one
         - two
         - three

Note: Changes in enum values are not very simple. For Mysql and Mariadb, migrations will be generated but in many cases custom modification in it are required. For Pgsql migrations for change in enum values will not be generated. It should be handled manually.

It will be ignored for database column schema (migrations) if x-db-type is provided.

Handling of numeric (#numeric, #MariaDb)

precision-default = 10 scale-default = 2

 test_table:
   properties:
     my_property:
       x-db-type: decimal(12,4)

DB-Result = decimal(12,4)

 test_table:
   properties:
     my_property:
       x-db-type: decimal(12)

DB-Result = decimal(12,2)

 test_table:
   properties:
     my_property:
       x-db-type: decimal

DB-Result = decimal(10,2)

Handling of timestamp database column data type

If field is defined as

created_at:
    type: string
    format: date-time # or datetime
    example: '2020-03-14T21:42:17Z'
    readOnly: true

then database type selected will be timestamp. This is by design. If datetime data type is needed, use x-db-type as

created_at:
    type: string
    format: date-time # or datetime
    example: '2020-03-14T21:42:17Z'
    x-db-type: datetime
    readOnly: true

Assumptions

When generating code from an OpenAPI description there are many possible ways to achive a fitting result. Thus, there are some assumptions and limitations that are currently applied to make this work. Here is a (possibly incomplete) list:

Other things to keep in mind:

Adding columns to existing tables

When adding new fields in the API models, new migrations will be generated to add these fields to the table. For a project that is already in production, it should be considered to adjust the generated migration to add default values for existing data records.

One case where this is important is the addition of a new column with NOT NULL contraint, which does not provide a default value. Such a migration will fail when the table is not empty:

$this->addColumn('{{%company}}', 'name', $this->string(128)->notNull());

Fails on a PostgreSQL database with

add column name string(128) NOT NULL to table {{%company}} ...Exception: SQLSTATE[23502]: Not null violation: 7 ERROR: column "name" contains null values

The solution would be to create the column, allowing NULL, set the value to a default and add the null constraint later.

$this->addColumn('{{%company}}', 'name', $this->string(128)->null());
$this->update('{{%company}}', ['name' => 'No name']);
$this->alterColumn('{{%company}}', 'name', $this->string(128)->notNull());

Screenshots

Gii Generator Form:

Gii Generator Form

Generated files:

Gii Generated Files

Development

To contribute or play around, steps to set up this project up locally are in CONTRIBUTING.md.

Support

Need help with your API project?

Professional support, consulting as well as software development services are available:

https://www.cebe.cc/en/contact

Development of this library is sponsored by cebe.:cloud: "Your Professional Deployment Platform".