Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ipontt opened this issue Mar 19, 2024 · 7 comments
Labels

Comments

@ipontt
Copy link

ipontt commented Mar 19, 2024

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?

@DerManoMann
Copy link
Collaborator

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.

@ipontt
Copy link
Author

ipontt commented Mar 21, 2024

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'];

@DerManoMann
Copy link
Collaborator

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...)

@ipontt
Copy link
Author

ipontt commented Mar 21, 2024

[...] 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
Copy link
Author

ipontt commented Mar 21, 2024

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?

@DerManoMann
Copy link
Collaborator

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 🦀

Yeah, nesting is always tricky - something recursive perhaps?

@ipontt
Copy link
Author

ipontt commented Mar 21, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants