swagger-php icon indicating copy to clipboard operation
swagger-php copied to clipboard

Passing a FQCN to a custom Response in order to generate the Examples object. Is it possible?

Open ipontt opened this issue 1 year ago • 7 comments

Hello, I'm trying to do something but I don't understand enough about swagger-php's internals to accomplish it. I'm not sure if it's a good idea or not either so I would like some guidance on the matter.

Basically, I want to create a custom response class that accepts a string parameter in its constructor.

Depending on this parameter, I'd like to construct the Response's example.

namespace App\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema]
class Post
{
    #[OA\Parameter]
    private int $post_id;

    #[OA\Parameter]
    private string $title;

    #[OA\Parameter]
    private string $body;
}
namespace App\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema]
class Comment
{
    #[OA\Parameter]
    private Post $post;

    #[OA\Parameter]
    private string $comment;
}
use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class ResourceResponse extends OA\Response
{
    public function __construct(public string $resourceType)
    {
        $expandedType = /* Probably something with the Generator */;

        parent::__construct(
            response: 200,
            description: 'Resource Response for ' . $resourceType,
            content: new OA\JsonContent(
                examples: new OA\Examples([
                    [
                        new OA\Example(
                            example: 'Resource Response',
                            summary: $resourceType . ' resource response',
                            value: [
                                'data' => [...] // expanded Post or Comment depending on $resourceType since #ref does not work with a wrapper.
                            ],
                        ),
                    ],
                ])
            );
        );
    }
}

Ideally, I'd then be able to use it like this:

#[OA\Get(path: '/posts', responses: [new ResourceResponse(Post::class)])]
public function index() { ... }

Is this possible?

ipontt avatar Mar 19 '24 01:03 ipontt

Makes sense to me. If you can write it verbatim using attributes there is no reason you couldn't wrap it in a custom attribute.

DerManoMann avatar Mar 19 '24 23:03 DerManoMann

I think I got it but I'm not sure if it's recommended. I have to basically map over the json-serialized array of OpenApi\Attributes\Schema objects.

It requires each OA\Property to have a default value and so far I'm still struggling to make it work with non-primitive properties (for example the Comment's $post property.

use Illuminate\Support\Facades\File;
use OpenApi\Generator;

use function base_path;
use function collect;

$schema_directories = File::directories(base_path('app/Schemas'); // ['/var/www/html/app/Schemas/Models', ...];
$api = Generator::scan($schema_directories);

$examples = collect($api->components->schemas)
    ->map->jsonSerialize()
    ->mapWithKeys(function ($shema) {
        $example = collect($schema->properties)
            ->mapWithKeys(fn ($property, $name) => [$name => $property->default])
            ->all();

        return [$schema->schema => $example];
    });

$examples->get('Post');          // ['post_id' => 1, 'title' => 'example title', 'body' => 'example body'];

ipontt avatar Mar 21 '24 17:03 ipontt

Hmm, I suppose that is now more a question of your business/domain logic. Not sure I can help with that. One off-topic question, though: are your schema files custom or how are they generated (Laravel questions...)

DerManoMann avatar Mar 21 '24 18:03 DerManoMann

[...] One off-topic question, though: are your schema files custom or how are they generated (Laravel questions...)

Laravel does not have a direct integration with swagger so I just arbitrarily placed them in an app/Schemas directory. I am using a package (darkaonline/l5-swagger) but that's mostly a wrapper on top of this package. The schema files are still plain PHP classes written manually.

ipontt avatar Mar 21 '24 22:03 ipontt

To avoid re-scanning the schemas every time that Response is initialized, I suppose I could make the resulting collection a static property, and just initialize it once, but that still doesn't solve the problem of nested Schemas.

use Attribute;
use Illuminate\Support\Facades\File;
use OpenApi\Attributes as OA;
use OpenApi\Generator;

use function base_path;
use function collect;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class ResourceResponse extends OA\Response
{
    public static $examples = null;

    public function __construct(string $resourceType)
    {
        $this->populateExamples();

        $expandedType = /* Probably something with the Generator */;

        parent::__construct(
            response: 200,
            description: 'Resource Response for ' . $resourceType,
            content: new OA\JsonContent(
                examples: new OA\Examples([
                    [
                        new OA\Example(
                            example: 'Resource Response',
                            summary: $resourceType . ' resource response',
                            value: [
                                'data' => static::$examples->get($resourceType),
                            ],
                        ),
                    ],
                ])
            );
        );
    }

    protected function populateExamples(): void
    {
        if ( static::$examples !== null ) return;

        $schema_directories = File::directories(base_path('app/Schemas'); // ['/var/www/html/app/Schemas/Models', ...];
        $api = Generator::scan($schema_directories);

        $examples = collect($api->components->schemas)
            ->map->jsonSerialize()
            ->mapWithKeys(function ($shema) {
                $example = collect($schema->properties)
                    ->mapWithKeys(fn ($property, $name) => [$name => $property->default])
                    ->all();

                return [$schema->schema => $example];
            });

        static::$examples = $examples;
    }
}

What do you mean about it depending on my business/domain logic?

ipontt avatar Mar 21 '24 23:03 ipontt

What do you mean about it depending on my business/domain logic?

Ah, for some reason I was thinking database when I saw App\Schemas :crab:

Yeah, nesting is always tricky - something recursive perhaps?

DerManoMann avatar Mar 21 '24 23:03 DerManoMann

Yeah, probably. Instead of

->mapWithKeys(fn ($property, $name) => [$name => $property->default])

It should be something like

->mapWithKeys(function ($property, $name) {
    if (/* property is primitive */) 
        return [$name => $property->default];

    // do something recursive with $property.
})

I'll need to tinker with it some more.

ipontt avatar Mar 21 '24 23:03 ipontt