Skip to content

Commit 7d6b830

Browse files
committed
Merge 4.3
2 parents 72b02af + 85f6269 commit 7d6b830

9 files changed

Lines changed: 148 additions & 6 deletions

File tree

src/Laravel/ApiPlatformProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,17 +749,22 @@ public function register(): void
749749
/** @var ConfigRepository */
750750
$config = $app['config'];
751751

752+
$graphQlEnabled = (bool) $config->get('api-platform.graphql.enabled', false);
753+
752754
return new SwaggerUiProcessor(
753755
urlGenerator: $app->make(UrlGeneratorInterface::class),
754756
normalizer: $app->make(NormalizerInterface::class),
755757
openApiOptions: $app->make(Options::class),
758+
formats: $config->get('api-platform.docs_formats', []),
756759
oauthClientId: $config->get('api-platform.swagger_ui.oauth.clientId'),
757760
oauthClientSecret: $config->get('api-platform.swagger_ui.oauth.clientSecret'),
758761
oauthPkce: $config->get('api-platform.swagger_ui.oauth.pkce', false),
759762
swaggerEnabled: $config->get('api-platform.swagger_ui.enabled', false),
760763
scalarEnabled: $config->get('api-platform.scalar.enabled', false),
761764
scalarExtraConfiguration: $config->get('api-platform.scalar.extra_configuration', []),
762765
redocEnabled: $config->get('api-platform.redoc.enabled', false),
766+
graphQlEnabled: $graphQlEnabled,
767+
graphiQlEnabled: $graphQlEnabled && (bool) $config->get('api-platform.graphiql.enabled', true),
763768
);
764769
});
765770

src/Laravel/Eloquent/Filter/OrderFilter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
4545
return $builder;
4646
}
4747

48-
$direction = strtoupper($values);
49-
if (!\in_array($direction, ['ASC', 'DESC'], true)) {
48+
$direction = strtolower($values);
49+
if (!\in_array($direction, ['asc', 'desc'], true)) {
5050
return $builder;
5151
}
5252

src/Laravel/Eloquent/Metadata/ModelMetadata.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,15 @@ public function getAttributes(Model $model): array
104104
];
105105
}
106106

107-
return $this->attributesLocalCache[$model::class] = array_merge($attributes, $this->getVirtualAttributes($model, $columns));
107+
$result = array_merge($attributes, $this->getVirtualAttributes($model, $columns));
108+
109+
// Don't cache an empty result for a missing table: the table may be created later
110+
// (e.g. by RefreshDatabase between MCP boot-time discovery and the actual request).
111+
if ([] === $result && !$schema->hasTable($table)) {
112+
return $result;
113+
}
114+
115+
return $this->attributesLocalCache[$model::class] = $result;
108116
}
109117

110118
/**

src/Laravel/State/SwaggerUiProcessor.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public function __construct(
4848
private readonly bool $scalarEnabled = false,
4949
private readonly array $scalarExtraConfiguration = [],
5050
private readonly bool $redocEnabled = false,
51+
private readonly bool $graphQlEnabled = false,
52+
private readonly bool $graphiQlEnabled = false,
5153
) {
5254
}
5355

@@ -62,8 +64,8 @@ public function process(mixed $openApi, Operation $operation, array $uriVariable
6264
'formats' => $this->formats,
6365
'title' => $openApi->getInfo()->getTitle(),
6466
'description' => $openApi->getInfo()->getDescription(),
65-
'originalRoute' => $request->attributes->get('_api_original_route', $request->attributes->get('_route')),
66-
'originalRouteParams' => $request->attributes->get('_api_original_route_params', $request->attributes->get('_route_params', [])),
67+
'originalRoute' => $request->attributes->get('_api_original_route') ?? $request->route()?->getName(),
68+
'originalRouteParams' => $request->attributes->get('_api_original_route_params') ?? $request->route()?->parameters() ?? [],
6769
];
6870

6971
$swaggerData = [
@@ -99,7 +101,15 @@ public function process(mixed $openApi, Operation $operation, array $uriVariable
99101

100102
$swaggerData['scalarExtraConfiguration'] = $this->scalarExtraConfiguration;
101103

102-
return new Response(view('api-platform::swagger-ui', $swaggerContext + ['swagger_data' => $swaggerData, 'ui' => $this->getUi()]), 200);
104+
return new Response(view('api-platform::swagger-ui', $swaggerContext + [
105+
'swagger_data' => $swaggerData,
106+
'ui' => $this->getUi(),
107+
'swaggerUiEnabled' => $this->swaggerEnabled,
108+
'redocEnabled' => $this->redocEnabled,
109+
'scalarEnabled' => $this->scalarEnabled,
110+
'graphQlEnabled' => $this->graphQlEnabled,
111+
'graphiQlEnabled' => $this->graphiQlEnabled,
112+
]), 200);
103113
}
104114

105115
/**
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Config\Repository;
18+
use Orchestra\Testbench\Concerns\WithWorkbench;
19+
use Orchestra\Testbench\TestCase;
20+
21+
class DocsSingleUiTest extends TestCase
22+
{
23+
use ApiTestAssertionsTrait;
24+
use WithWorkbench;
25+
26+
protected function defineEnvironment($app): void
27+
{
28+
tap($app['config'], static function (Repository $config): void {
29+
$config->set('api-platform.swagger_ui.enabled', true);
30+
$config->set('api-platform.redoc.enabled', false);
31+
$config->set('api-platform.scalar.enabled', false);
32+
});
33+
}
34+
35+
public function testHtmlDocsHasNoOtherUiLinksWhenOnlyOneUiEnabled(): void
36+
{
37+
$res = $this->get('/api/docs', headers: ['accept' => 'text/html']);
38+
$res->assertOk();
39+
$content = (string) $res->getContent();
40+
41+
$this->assertStringContainsString('init-swagger-ui.js', $content);
42+
$this->assertStringContainsString('id="formats"', $content);
43+
$this->assertStringNotContainsString('>Swagger UI</a>', $content);
44+
$this->assertStringNotContainsString('>ReDoc</a>', $content);
45+
$this->assertStringNotContainsString('>Scalar</a>', $content);
46+
}
47+
}

src/Laravel/Tests/DocsTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,40 @@ public function testJsonLdAccept(): void
4949
$this->assertArrayHasKey('@context', $res->json());
5050
$this->assertSame('application/ld+json; charset=utf-8', $res->headers->get('content-type'));
5151
}
52+
53+
public function testHtmlDocsRendersSwaggerUiByDefault(): void
54+
{
55+
$res = $this->get('/api/docs', headers: ['accept' => 'text/html']);
56+
$res->assertOk();
57+
$content = (string) $res->getContent();
58+
59+
$this->assertStringContainsString('init-swagger-ui.js', $content);
60+
$this->assertStringContainsString('id="formats"', $content);
61+
$this->assertStringContainsString('>ReDoc</a>', $content);
62+
$this->assertStringContainsString('>Scalar</a>', $content);
63+
$this->assertStringNotContainsString('>Swagger UI</a>', $content);
64+
}
65+
66+
public function testHtmlDocsRendersRedocWhenRequested(): void
67+
{
68+
$res = $this->get('/api/docs?ui=redoc', headers: ['accept' => 'text/html']);
69+
$res->assertOk();
70+
$content = (string) $res->getContent();
71+
72+
$this->assertStringContainsString('init-redoc-ui.js', $content);
73+
$this->assertStringContainsString('id="formats"', $content);
74+
$this->assertStringContainsString('>Swagger UI</a>', $content);
75+
$this->assertStringContainsString('>Scalar</a>', $content);
76+
$this->assertStringNotContainsString('>ReDoc</a>', $content);
77+
}
78+
79+
public function testHtmlDocsRendersScalarWithoutFooterWhenRequested(): void
80+
{
81+
$res = $this->get('/api/docs?ui=scalar', headers: ['accept' => 'text/html']);
82+
$res->assertOk();
83+
$content = (string) $res->getContent();
84+
85+
$this->assertStringContainsString('init-scalar-ui.js', $content);
86+
$this->assertStringNotContainsString('id="formats"', $content);
87+
}
5288
}

src/Laravel/resources/views/swagger-ui.blade.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,26 @@
213213
@endif
214214

215215
<div id="swagger-ui" class="api-platform"></div>
216+
217+
@if ($ui !== 'scalar')
218+
<div class="swagger-ui" id="formats">
219+
<div class="information-container wrapper">
220+
<div class="info">
221+
Available formats:
222+
@foreach (array_keys($formats) as $format)
223+
<a href="{{ route($originalRoute, array_merge($originalRouteParams, ['_format' => '.'.$format])) }}">{{ $format }}</a>
224+
@endforeach
225+
<br>
226+
Other API docs:
227+
@if ($swaggerUiEnabled && $ui !== 'swagger')<a href="{{ route($originalRoute, $originalRouteParams) }}">Swagger UI</a>@endif
228+
@if ($redocEnabled && $ui !== 'redoc')<a href="{{ route($originalRoute, array_merge($originalRouteParams, ['ui' => 'redoc'])) }}">ReDoc</a>@endif
229+
@if ($scalarEnabled && $ui !== 'scalar')<a href="{{ route($originalRoute, array_merge($originalRouteParams, ['ui' => 'scalar'])) }}">Scalar</a>@endif
230+
@if (!$graphQlEnabled || $graphiQlEnabled)<a @if ($graphiQlEnabled)href="{{ route('api_graphiql') }}"@endif class="graphiql-link">GraphiQL</a>@endif
231+
</div>
232+
</div>
233+
</div>
234+
@endif
235+
216236
@if ($ui === 'scalar')
217237
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
218238
<script src="/vendor/api-platform/init-scalar-ui.js"></script>

src/Mcp/Capability/Registry/Loader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public function load(RegistryInterface $registry): void
6363
$registry->registerTool(
6464
new Tool(
6565
name: $mcp->getName(),
66+
title: $mcp->getTitle(),
6667
inputSchema: $inputSchema->getArrayCopy(),
6768
description: $mcp->getDescription(),
6869
annotations: $mcp->getAnnotations() ? ToolAnnotations::fromArray($mcp->getAnnotations()) : null,

src/Metadata/McpTool.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class McpTool extends HttpOperation
2727
{
2828
/**
2929
* @param string|null $name The name of the tool (defaults to the method name)
30+
* @param string|null $title Optional human-readable title for display in UI
3031
* @param string|null $description The description of the tool (defaults to the DocBlock/inferred)
3132
* @param bool|null $structuredContent Whether to include structured content in the response (defaults to true)
3233
* @param mixed|null $annotations Optional annotations describing tool behavior
@@ -90,6 +91,7 @@ class McpTool extends HttpOperation
9091
*/
9192
public function __construct(
9293
?string $name = null,
94+
protected ?string $title = null,
9395
?string $description = null,
9496
protected ?bool $structuredContent = null,
9597
protected mixed $annotations = null,
@@ -321,4 +323,17 @@ public function withStructuredContent(?bool $structuredContent): static
321323

322324
return $self;
323325
}
326+
327+
public function getTitle(): ?string
328+
{
329+
return $this->title;
330+
}
331+
332+
public function withTitle(?string $title): static
333+
{
334+
$self = clone $this;
335+
$self->title = $title;
336+
337+
return $self;
338+
}
324339
}

0 commit comments

Comments
 (0)