Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
"jangregor/phpstan-prophecy": "^2.1.11",
"justinrainbow/json-schema": "^6.5.2",
"laravel/framework": "^11.0 || ^12.0 || ^13.0",
"mcp/sdk": "^0.6",
"mcp/sdk": "^0.7",
"orchestra/testbench": "^10.9 || ^11.0",
"phpspec/prophecy-phpunit": "^2.2",
"phpstan/extension-installer": "^1.1",
Expand Down
4 changes: 3 additions & 1 deletion src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use Psr\Log\LoggerInterface;
use Symfony\AI\McpBundle\Controller\McpController;
use Symfony\AI\McpBundle\Http\MiddlewareFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\RequestStack;
Expand Down Expand Up @@ -1342,7 +1343,8 @@ private function registerMcp(): void
$psrHttpFactory,
$httpFoundationFactory,
$psr17Factory,
$psr17Factory
$psr17Factory,
new MiddlewareFactory()
);
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/Mcp/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"php": ">=8.2",
"api-platform/metadata": "^5.0@alpha",
"api-platform/json-schema": "^5.0@alpha",
"mcp/sdk": "^0.6",
"mcp/sdk": "^0.7",
"symfony/object-mapper": "^7.4 || ^8.0",
"symfony/polyfill-php85": "^1.32"
},
Expand Down
23 changes: 21 additions & 2 deletions src/Metadata/Tests/Util/IriHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
*/
class IriHelperTest extends TestCase
{
private const OBJECTS_PATH = '/objects';
private const PAGE_PARAMETER_NAME = 'page';
private const BRACKETED_INDEX_VALUE = 'x[0]';

public function testHelpers(): void
{
$parsed = [
Expand Down Expand Up @@ -93,18 +97,33 @@ public function testHelpersPreserveNestedArrayQueryParameters(): void
public function testHelpersCollapseSimpleListQueryParameters(): void
{
$parts = [
'path' => '/objects',
'path' => self::OBJECTS_PATH,
'query' => '',
];
$parameters = [
'foo' => ['a', 'b'],
];

$iri = IriHelper::createIri($parts, $parameters, 'page', 2.);
$iri = IriHelper::createIri($parts, $parameters, self::PAGE_PARAMETER_NAME, 2.);

$this->assertSame('/objects?foo%5B%5D=a&foo%5B%5D=b&page=2', $iri);
}

public function testCreateIriPreservesBracketedNumericIndexesInQueryValues(): void
{
$parts = [
'path' => self::OBJECTS_PATH,
'query' => '',
];
$parameters = [
'v' => self::BRACKETED_INDEX_VALUE,
];

$iri = IriHelper::createIri($parts, $parameters, self::PAGE_PARAMETER_NAME, 2.);

$this->assertSame('/objects?v=x%5B0%5D&page=2', $iri);
}

public function testParseIriWithInvalidUrl(): void
{
$this->expectException(InvalidArgumentException::class);
Expand Down
29 changes: 24 additions & 5 deletions src/Metadata/Util/IriHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
*/
final class IriHelper
{
private const EMPTY_BRACKET_REPLACEMENT = '%5B%5D';
private const INDEXED_LEAF_BRACKET_PATTERN = '/%5B\d+%5D(?!%5B)/';
private const QUERY_ASSIGNMENT_SEPARATOR = '=';
private const QUERY_SEPARATOR = '&';

private function __construct()
{
}
Expand Down Expand Up @@ -64,11 +69,8 @@ public static function createIri(array $parts, array $parameters, ?string $pageP
$parameters[$pageParameterName] = $page;
}

$query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986);
// Only collapse a numeric index when it is the leaf segment of a bracket chain
// (a simple list element). Collapsing a non-leaf index would merge distinct keys of a
// nested array into separate elements (e.g. filters[0][a] must stay filters[0][a]).
$parts['query'] = preg_replace('/%5B\d+%5D(?!%5B)/', '%5B%5D', $query);
$query = http_build_query($parameters, '', self::QUERY_SEPARATOR, \PHP_QUERY_RFC3986);
$parts['query'] = self::collapseNumericLeafIndexesInQueryKeys($query);

$url = '';
if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) {
Expand Down Expand Up @@ -110,4 +112,21 @@ public static function createIri(array $parts, array $parameters, ?string $pageP

return $url;
}

private static function collapseNumericLeafIndexesInQueryKeys(string $query): string
{
$parameters = explode(self::QUERY_SEPARATOR, $query);

foreach ($parameters as $index => $parameter) {
[$key, $value] = explode(self::QUERY_ASSIGNMENT_SEPARATOR, $parameter, 2) + [1 => null];

// Only collapse a numeric index when it is the leaf segment of a bracket chain
// (a simple list element). Collapsing a non-leaf index would merge distinct keys of a
// nested array into separate elements (e.g. filters[0][a] must stay filters[0][a]).
$key = preg_replace(self::INDEXED_LEAF_BRACKET_PATTERN, self::EMPTY_BRACKET_REPLACEMENT, $key);
$parameters[$index] = null === $value ? $key : $key.self::QUERY_ASSIGNMENT_SEPARATOR.$value;
}

return implode(self::QUERY_SEPARATOR, $parameters);
}
}
Loading