rupadana / filament-api-service

A simple api service for supporting filamentphp
https://filamentphp.com/plugins/rupadana-api-service
MIT License
95 stars 23 forks source link

[Feature]: Swagger API docs #44

Open eelco2k opened 3 months ago

eelco2k commented 3 months ago

What happened?

Use PHP Swagger API docs generator with https://github.com/DarkaOnLine/L5-Swagger and https://github.com/zircote/swagger-php to add these codeblocks to the stubs.

use OpenApi\Attributes as OA;

#[OA\Info(
    title:"API Documentation",
    version: "0.1",
    contact: new OA\Contact(name: "", email: "")
)]
#[OA\Server(
    description: "API Server",
    url: "http://localhost:8000/api/"
)]

#[OA\SecurityScheme(
    name: "bearerAuth",
    securityScheme: "bearerAuth",
    type: "http",
    scheme: "bearer",
    description: "Enter JWT Token",
    in: "header"
)]

#[OA\PathParameter(
    name: 'tenant',
    parameter: 'tenant',
    description: 'ID of the tenant',
    schema: new OA\Schema(type: 'integer'),
    required: TENANT_AWARENESS,
    in: "path",
)]

Most of the parameters can be set as custom CONSTANTS when the plugin is configured via config variables in l5-swagger.php

define('TENANT_AWARENESS', config('api-service.tenancy.awareness'); or so...

check if tenancy for resource is enabled set constant per resource in for example the BrandApiService.php


#[OA\Tag(
    name: "Brands",
    description: "API Endpoints of Brands",
)]

class BrandApiService extends ApiService
{

public static function handlers(): array
    {
        if (
            ApiService::isTenancyEnabled() &&
            ApiService::tenancyAwareness() &&
            static::getResource()::isScopedToTenant()
        ) {
            define('TENANT_AWARENESS_BRANDS', true);
        } else {
            define('TENANT_AWARENESS_BRANDS', false);
        }

        $handlers =  [
            Handlers\CreateHandler::class,
            Handlers\UpdateHandler::class,
            Handlers\DeleteHandler::class,
            Handlers\PaginationHandler::class,
            Handlers\DetailHandler::class
        ];

        foreach($handlers as $handler) {
            if(app($handler)->isPublic()) {
                define('RESOURCE_PUBLIC_BRANDS_' . Str::upper(app($handler)->getKebabClassName()), true);
            } else {
                define('RESOURCE_PUBLIC_BRANDS_' . Str::upper(app($handler)->getKebabClassName()), false);
            }
        }

        return $handlers;
    }
}

as an example for the different resource API Handlers.php (CreateHandler.php, UpdateHandler.php etc...) :


use OpenApi\Attributes as OA;

#[OA\Post(
    path: "/admin/" . ((TENANT_AWARENESS_BRANDS) ? "{tenant}/" : "") . "brands",
    operationId: "storeBrand",
    tags: ["Brands"],
    summary: "Store new Brand",
    description: "Returns inserted brand data",
    security: (!RESOURCE_PUBLIC_BRANDS_CREATE) ?  [["bearerAuth" => []]]: null,
    parameters: [
        (TENANT_AWARENESS_BRANDS) ? new OA\Parameter(ref: "#/components/parameters/tenant") : null,
    ],
    requestBody: new OA\RequestBody(
        required: true,
        content: new OA\JsonContent(ref: "#/components/schemas/BrandTransformer/properties/data/items")
    ),
    responses: [
        new OA\Response(response: 200, description: 'Operation succesful', content: new OA\JsonContent(type: "object", properties: [
            new OA\Property(property: "message", type: "string", example: "Successfully Inserted Resource"),
            new OA\Property(property: "data", type: "object", ref: "#/components/schemas/BrandTransformer/properties/data/items")
        ])),
        new OA\Response(response: 400, description: 'Bad Request'),
        new OA\Response(response: 401, description: 'Unauthenticated'),
        new OA\Response(response: 403, description: 'Forbidden'),
        new OA\Response(response: 404, description: 'Resource not Found'),
    ]
)]

and

#[OA\Get(
    path: "/admin/" . ((TENANT_AWARENESS_BRANDS) ? "{tenant}/" : "") . "brands/{id}",
    operationId: "getBrandDetail",
    tags: ["Brands"],
    summary: "Get detail of Brand",
    description: "Returns detail of Brand",
    security: (!RESOURCE_PUBLIC_BRANDS_DETAIL) ?  [["bearerAuth" => []]]: null,
    parameters: [
        (TENANT_AWARENESS_BRANDS) ? new OA\Parameter(ref: "#/components/parameters/tenant") : null,
        new OA\Parameter(
            name: "id",
            description: "Brand ID",
            required: true,
            in: "path",
            schema: new OA\Schema(type: "integer"),
            example: "", // OA\Examples(example="int", value="0", summary="An int value."),
        ),
        new OA\Parameter(
            name: "page[offset]",
            description: "Pagination offset option",
            required: false,
            in: "query",
            schema: new OA\Schema(type: "integer"),
            example: "", // OA\Examples(example="int", value="0", summary="An int value."),
        ),
        new OA\Parameter(
            name: "page[limit]",
            description: "Pagination limit option",
            required: false,
            in: "query",
            schema: new OA\Schema(type: "integer"),
            example: "", // OA\Examples(example="int", value="0", summary="An int value."),
        ),
        new OA\Parameter(
            name: "sort",
            description: "Sorting",
            required: false,
            in: "query",
            schema: new OA\Schema(type: "string"),
            example: "", // @OA\Examples(example="string", value="-created,name", summary="A comma separated value"),
        ),
        new OA\Parameter(
            name: "include",
            description: "Include Relationships",
            required: false,
            in: "query",
            schema: new OA\Schema(type: "string"),
            example: "", // @OA\Examples(example="string", value="order,user", summary="A comma separated value of relationships"),
        ),
    ],
    responses: [
        new OA\Response(response: 200, description: 'Operation succesful', content:  new OA\JsonContent( type: "object", ref: "#/components/schemas/BrandTransformer/properties/data/items")),
        new OA\Response(response: 400, description: 'Bad Request'),
        new OA\Response(response: 401, description: 'Unauthenticated'),
        new OA\Response(response: 403, description: 'Forbidden'),
        new OA\Response(response: 404, description: 'Resource not Found'),
    ]
)]

and BrandTransformer.php

#[OA\Schema(
    title: "BrandTransformer",
    description: "Brands API Transformer",
    xml: new OA\XML(name: "BrandTransformer"),
)]

and the transformer schema

 #[OA\Property(
        property: "data",
        type: "array",
        items: new OA\Items(
            properties: [
                new OA\Property(property: "id", type: "integer", title: "ID", description: "id of the brand", example: ""),
               // add your own...
                new OA\Property(property: "created_at", type: "string", title: "Created at", description: "Created at Datetime of Product", example: ""),
                new OA\Property(property: "updated_at", type: "string", title: "Updated at", description: "Updated at Datetime of Product", example: ""),
            ]
        ),
    )]
    #[OA\Property(
        property: "meta",
        type: "object",
        properties: [
            new OA\Property(property: "current_page", type: "integer", title: "Current Page", description: "current page of brand", example: ""),
            new OA\Property(property: "from", type: "integer", title: "", description: "", example: ""),
            new OA\Property(property: "last_page", type: "integer", title: "", description: "", example: ""),
            new OA\Property(property: "path", type: "string", title: "", description: "", example: ""),
            new OA\Property(property: "per_page", type: "integer", title: "", description: "", example: ""),
            new OA\Property(property: "to", type: "integer", title: "", description: "", example: ""),
            new OA\Property(property: "total", type: "integer", title: "", description: "", example: ""),
            new OA\Property(property: "", type: "integer", title: "", description: "", example: ""),
            new OA\Property(
                property: "links",
                type: "array",
                items: new OA\Items(properties: [
                    new OA\Property(property: "url", type: "string", description: "URL of Pagination"),
                    new OA\Property(property: "label", type: "string", description: "Label of Pagination"),
                    new OA\Property(property: "active", type: "bool", description: "Pagination is Active"),
                ])
            )
        ]
    )]
    #[OA\Property(
        property: "links",
        type: "object",
        properties: [
            new OA\Property(property: "first", type: "string", description: "first page link URL of brand", example: ""),
            new OA\Property(property: "last", type: "string", description: "last page link URL of brand", example: ""),
            new OA\Property(property: "prev", type: "string", description: "prev page link URL of brand", example: ""),
            new OA\Property(property: "next", type: "string", description: "next page link URL of brand", example: ""),
        ])
    ]

How to reproduce the bug

Implement OpenApi\Attributes;

Package Version

3.2

PHP Version

8.3

Laravel Version

10.0

Which operating systems does with happen with?

No response

Notes

This is just a start and code is not always cleaned up and i'm not quite sure everything is covered like response codes, panel prefix as CONST so it can be added in path: etc.... I could make time to add these features and do a PR if you prefer.

rupadana commented 3 months ago

yeah its a good idea, but i think this must be optional. The documentation will generated if developer parse argument, like --swagger maybe. so the artisan command look like php artisan make:filament-api-service BlogResource --swagger.

and i don't like this part define('TENANT_AWARENESS_BRANDS', true);. The BRANDS is make confused. if you want to do this, we can use resource property on every handler. maybe adding this method to Handlers class :

public static function isTenantAwarenessEnabled() : bool 
{
   return static::getResource()::isScopedToTenant();
}

so you can call it while registering the routes.

eelco2k commented 3 months ago

I was thinking of putting it in a /Virtual/ folder with some empty classes, that way it is separted from actual code. This also means that you can regenerate API docs of one or more resources based on those api docs stubs again and again.

You are absolutely correct that the BRANDS is not okay. it was just an example and can be dynamically fetched from classname of resource.

And yes, an extra static function on the handler class isTenantAwerenessEnabled() is a good idea.

I just posted a rough WiP.. but i will add your considerations :)

eelco2k commented 3 months ago

first work on swagger generation. I created it as a separate command instead of --swagger option in the filament-api-service for now.

rough work is in this feature branch as pull request https://github.com/rupadana/filament-api-service/pull/51

POMXARK commented 1 month ago

thank you for the tools. eelco2k fix it critical errors

        $baseResourceSourcePath =  (string) str($resourceClass)->prepend('/')->prepend(base_path($resourcePath))
            ->replace('\\', '/')->replace('//', '/')
            ->replace('App', 'app'); // default

and

<?php

namespace {{ namespace }}\{{ resourceClass }}\Transformers;

use OpenApi\Attributes as OAT;

#[OAT\Schema(
    schema: "{{ transformerName }}",
    title: "{{ transformerName }}",
    description: "{{ modelClass }} API Transformer",
    xml: new OAT\Xml(name: "{{ transformerName }}"),
)]

->replace('App', 'app')

Xml

It would be nice to add an example of use

php artisan make:filament-resource User
php artisan make:filament-api-service UserResource
php artisan make:filament-api-transformer UserResource

add in UserResource

    /**
     * @return string|null
     */
    public static function getApiTransformer(): ?string
    {
        return UserTransformer::class;
    }
php artisan make:filament-api-docs UserResource
php artisan l5-swagger:generate

I used the package until the patch was accepted

composer require cweagans/composer-patches

and composer,json

    "extra": {
        "laravel": {
            "dont-discover": []
        },
        "patches": {
            "rupadana/filament-api-service": {
                "add command filament-api-docs (for generate swagger)": "patches/rupadana/filament-api-service/filament-api-docs.txt"
            }
        },
        "enable-patching": true
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "source",
        "sort-packages": true,
        "allow-plugins": {
            "pestphp/pest-plugin": true,
            "php-http/discovery": true,
            "cweagans/composer-patches": true
        }
    },

filament-api-docs.txt

Thanks for the work. Good luck in your development!

eelco2k commented 1 month ago

@POMXARK I've added your patches. hopefully everything works now.