Passing a FQCN to a custom Response in order to generate the Examples object. Is it possible?
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?
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.
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'];
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...)
[...] 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.
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?
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?
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.