*/
public function getListeners(): iterable
{
diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php
index 6835762315415..09526fde6c5cd 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php
@@ -11,9 +11,11 @@
namespace Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
@@ -23,7 +25,7 @@
*
* @author Nicolas Grekas
*/
-class LazyFirewallContext extends FirewallContext
+class LazyFirewallContext extends FirewallContext implements FirewallListenerInterface
{
public function __construct(
iterable $listeners,
@@ -40,19 +42,26 @@ public function getListeners(): iterable
return [$this];
}
- public function __invoke(RequestEvent $event): void
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
{
$listeners = [];
$request = $event->getRequest();
$lazy = $request->isMethodCacheable();
foreach (parent::getListeners() as $listener) {
- if (!$lazy || !$listener instanceof FirewallListenerInterface) {
+ if (!$listener instanceof FirewallListenerInterface) {
+ trigger_deprecation('symfony/security-http', '7.4', 'Using a callable as firewall listener is deprecated, extend "%s" or implement "%s" instead.', AbstractListener::class, FirewallListenerInterface::class);
+
$listeners[] = $listener;
- $lazy = $lazy && $listener instanceof FirewallListenerInterface;
+ $lazy = false;
} elseif (false !== $supports = $listener->supports($request)) {
$listeners[] = [$listener, 'authenticate'];
- $lazy = null === $supports;
+ $lazy = $lazy && null === $supports;
}
}
@@ -75,4 +84,19 @@ public function __invoke(RequestEvent $event): void
}
});
}
+
+ public static function getPriority(): int
+ {
+ return 0;
+ }
+
+ /**
+ * @deprecated since Symfony 7.4, to be removed in 8.0
+ */
+ public function __invoke(RequestEvent $event): void
+ {
+ trigger_deprecation('symfony/security-bundle', '7.4', 'The "%s()" method is deprecated since Symfony 7.4 and will be removed in 8.0.', __METHOD__);
+
+ $this->authenticate($event);
+ }
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
index 5528c9b7a8fc7..053bf25f5485c 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
@@ -32,6 +32,8 @@
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\User\InMemoryUser;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Symfony\Component\VarDumper\Caster\ClassStub;
@@ -193,8 +195,18 @@ public function testGetListeners()
$request = new Request();
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
$event->setResponse($response = new Response());
- $listener = function ($e) use ($event, &$listenerCalled) {
- $listenerCalled += $e === $event;
+ $listener = new class extends AbstractListener {
+ public int $callCount = 0;
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ ++$this->callCount;
+ }
};
$firewallMap = $this
->getMockBuilder(FirewallMap::class)
@@ -217,9 +229,9 @@ public function testGetListeners()
$collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true);
$collector->collect($request, $response);
- $this->assertNotEmpty($collected = $collector->getListeners()[0]);
+ $this->assertCount(1, $collector->getListeners());
$collector->lateCollect();
- $this->assertSame(1, $listenerCalled);
+ $this->assertSame(1, $listener->callCount);
}
public function testCollectCollectsDecisionLogWhenStrategyIsAffirmative()
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php
index 4ab483a28f38a..db6e8a0e548c8 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php
@@ -29,7 +29,9 @@
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
/**
@@ -41,9 +43,19 @@ public function testOnKernelRequestRecordsListeners()
{
$request = new Request();
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
- $event->setResponse($response = new Response());
- $listener = function ($e) use ($event, &$listenerCalled) {
- $listenerCalled += $e === $event;
+ $event->setResponse(new Response());
+ $listener = new class extends AbstractListener {
+ public int $callCount = 0;
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ ++$this->callCount;
+ }
};
$firewallMap = $this->createMock(FirewallMap::class);
$firewallMap
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
index d0f3549ab8f09..4999fff7347db 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
@@ -29,6 +29,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\InMemoryUserChecker;
@@ -883,7 +884,7 @@ public function testCustomHasherWithMigrateFrom()
$container->loadFromExtension('security', [
'password_hashers' => [
'legacy' => 'md5',
- 'App\User' => [
+ TestUserChecker::class => [
'id' => 'App\Security\CustomHasher',
'migrate_from' => 'legacy',
],
@@ -895,11 +896,19 @@ public function testCustomHasherWithMigrateFrom()
$hashersMap = $container->getDefinition('security.password_hasher_factory')->getArgument(0);
- $this->assertArrayHasKey('App\User', $hashersMap);
- $this->assertEquals($hashersMap['App\User'], [
+ $this->assertArrayHasKey(TestUserChecker::class, $hashersMap);
+ $this->assertEquals($hashersMap[TestUserChecker::class], [
'instance' => new Reference('App\Security\CustomHasher'),
'migrate_from' => ['legacy'],
]);
+
+ $legacyAlias = \sprintf('%s $%s', PasswordHasherInterface::class, 'legacy');
+ $this->assertTrue($container->hasAlias($legacyAlias));
+ $definition = $container->getDefinition((string) $container->getAlias($legacyAlias));
+ $this->assertSame(PasswordHasherInterface::class, $definition->getClass());
+
+ $this->assertFalse($container->hasAlias(\sprintf('%s $%s', PasswordHasherInterface::class, 'symfonyBundleSecurityBundleTestsDependencyInjectionTestUserChecker')));
+ $this->assertFalse($container->hasAlias(\sprintf('.%s $%s', PasswordHasherInterface::class, TestUserChecker::class)));
}
public function testAuthenticatorsDecoration()
diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json
index 7459b0175b95f..cbad87a62861c 100644
--- a/src/Symfony/Bundle/SecurityBundle/composer.json
+++ b/src/Symfony/Bundle/SecurityBundle/composer.json
@@ -19,37 +19,38 @@
"php": ">=8.2",
"composer-runtime-api": ">=2.1",
"ext-xml": "*",
- "symfony/clock": "^6.4|^7.0",
- "symfony/config": "^7.3",
- "symfony/dependency-injection": "^6.4.11|^7.1.4",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/password-hasher": "^6.4|^7.0",
- "symfony/security-core": "^7.3",
- "symfony/security-csrf": "^6.4|^7.0",
- "symfony/security-http": "^7.3",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^7.3|^8.0",
+ "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/password-hasher": "^6.4|^7.0|^8.0",
+ "symfony/security-core": "^7.3|^8.0",
+ "symfony/security-csrf": "^6.4|^7.0|^8.0",
+ "symfony/security-http": "^7.3|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/asset": "^6.4|^7.0",
- "symfony/browser-kit": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/css-selector": "^6.4|^7.0",
- "symfony/dom-crawler": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/form": "^6.4|^7.0",
- "symfony/framework-bundle": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/ldap": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/rate-limiter": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
- "symfony/twig-bundle": "^6.4|^7.0",
- "symfony/twig-bridge": "^6.4|^7.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/yaml": "^6.4|^7.0",
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/form": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/ldap": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/twig-bundle": "^6.4|^7.0|^8.0",
+ "symfony/twig-bridge": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12",
"web-token/jwt-library": "^3.3.2|^4.0"
},
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
index 812ac1f666978..0105c71775903 100644
--- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
+++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
@@ -46,6 +46,7 @@
use Twig\Extension\OptimizerExtension;
use Twig\Extension\StagingExtension;
use Twig\ExtensionSet;
+use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
@@ -65,6 +66,7 @@
->tag('container.preload', ['class' => EscaperExtension::class])
->tag('container.preload', ['class' => OptimizerExtension::class])
->tag('container.preload', ['class' => StagingExtension::class])
+ ->tag('container.preload', ['class' => BinaryOperatorExpressionParser::class])
->tag('container.preload', ['class' => ExtensionSet::class])
->tag('container.preload', ['class' => Template::class])
->tag('container.preload', ['class' => TemplateWrapper::class])
diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json
index 221a7f471290e..6fc30ca79a8cd 100644
--- a/src/Symfony/Bundle/TwigBundle/composer.json
+++ b/src/Symfony/Bundle/TwigBundle/composer.json
@@ -18,24 +18,24 @@
"require": {
"php": ">=8.2",
"composer-runtime-api": ">=2.1",
- "symfony/config": "^7.3",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/twig-bridge": "^7.3",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/config": "^7.3|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/twig-bridge": "^7.3|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12"
},
"require-dev": {
- "symfony/asset": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
- "symfony/form": "^6.4|^7.0",
- "symfony/routing": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
- "symfony/yaml": "^6.4|^7.0",
- "symfony/framework-bundle": "^6.4|^7.0",
- "symfony/web-link": "^6.4|^7.0"
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/form": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/web-link": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/framework-bundle": "<6.4",
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
index 46175d1d1f82e..09e022be922b0 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
@@ -16,7 +16,7 @@
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
- trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profile.php" instead.');
+ trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profiler.php" instead.');
break;
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
index 81b471d228c05..d0383ee8fbef9 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
@@ -16,7 +16,7 @@
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
- trigger_deprecation('symfony/routing', '7.3', 'The "xdt.xml" routing configuration file is deprecated, import "xdt.php" instead.');
+ trigger_deprecation('symfony/routing', '7.3', 'The "wdt.xml" routing configuration file is deprecated, import "wdt.php" instead.');
break;
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
index 91e6dc05e658c..5adfd27796acf 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
@@ -144,7 +144,7 @@
var ajaxToolbarPanel = document.querySelector('.sf-toolbar-block-ajax');
if (requestStack.length) {
- ajaxToolbarPanel.style.display = 'block';
+ ajaxToolbarPanel.style.display = '';
} else {
ajaxToolbarPanel.style.display = 'none';
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json
index 00269dd279d45..4bcc0e01c4fc0 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/composer.json
+++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json
@@ -18,19 +18,19 @@
"require": {
"php": ">=8.2",
"composer-runtime-api": ">=2.1",
- "symfony/config": "^7.3",
+ "symfony/config": "^7.3|^8.0",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/framework-bundle": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/routing": "^6.4|^7.0",
- "symfony/twig-bundle": "^6.4|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/twig-bundle": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12"
},
"require-dev": {
- "symfony/browser-kit": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/css-selector": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0"
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/form": "<6.4",
diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json
index e8e1368f0e01c..d0107ed898d70 100644
--- a/src/Symfony/Component/Asset/composer.json
+++ b/src/Symfony/Component/Asset/composer.json
@@ -19,9 +19,9 @@
"php": ">=8.2"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
diff --git a/src/Symfony/Component/AssetMapper/composer.json b/src/Symfony/Component/AssetMapper/composer.json
index 1286eefc09081..076f3bb9769d2 100644
--- a/src/Symfony/Component/AssetMapper/composer.json
+++ b/src/Symfony/Component/AssetMapper/composer.json
@@ -19,20 +19,20 @@
"php": ">=8.2",
"composer/semver": "^3.0",
"symfony/deprecation-contracts": "^2.1|^3",
- "symfony/filesystem": "^7.1",
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/filesystem": "^7.1|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/asset": "^6.4|^7.0",
- "symfony/browser-kit": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher-contracts": "^3.0",
- "symfony/finder": "^6.4|^7.0",
- "symfony/framework-bundle": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/web-link": "^6.4|^7.0"
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/web-link": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/framework-bundle": "<6.4"
diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json
index e145984e64eab..b2e6761dab249 100644
--- a/src/Symfony/Component/BrowserKit/composer.json
+++ b/src/Symfony/Component/BrowserKit/composer.json
@@ -17,13 +17,13 @@
],
"require": {
"php": ">=8.2",
- "symfony/dom-crawler": "^6.4|^7.0"
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/css-selector": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0"
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\BrowserKit\\": "" },
diff --git a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
index cea761f5f99ac..e2cebc77f1015 100644
--- a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
+++ b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
@@ -120,9 +120,9 @@ public function testLateCollect()
$stats = $collector->getStatistics();
$this->assertGreaterThan(0, $stats[self::INSTANCE_NAME]['time']);
- $this->assertEquals($stats[self::INSTANCE_NAME]['hits'], 0, 'hits');
- $this->assertEquals($stats[self::INSTANCE_NAME]['misses'], 1, 'misses');
- $this->assertEquals($stats[self::INSTANCE_NAME]['calls'], 1, 'calls');
+ $this->assertEquals(0, $stats[self::INSTANCE_NAME]['hits'], 'hits');
+ $this->assertEquals(1, $stats[self::INSTANCE_NAME]['misses'], 'misses');
+ $this->assertEquals(1, $stats[self::INSTANCE_NAME]['calls'], 'calls');
$this->assertInstanceOf(Data::class, $collector->getCalls());
}
diff --git a/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php
new file mode 100644
index 0000000000000..367f82f7bb2b6
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits\Relay;
+
+if (version_compare(phpversion('relay'), '0.11', '>=')) {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($arg = null): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+} else {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($schedule = false): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/RelayProxy.php b/src/Symfony/Component/Cache/Traits/RelayProxy.php
index e0ca8873a0182..b6d48dd543dba 100644
--- a/src/Symfony/Component/Cache/Traits/RelayProxy.php
+++ b/src/Symfony/Component/Cache/Traits/RelayProxy.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Traits;
+use Symfony\Component\Cache\Traits\Relay\BgsaveTrait;
use Symfony\Component\Cache\Traits\Relay\CopyTrait;
use Symfony\Component\Cache\Traits\Relay\GeosearchTrait;
use Symfony\Component\Cache\Traits\Relay\GetrangeTrait;
@@ -31,6 +32,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
*/
class RelayProxy extends \Relay\Relay implements ResetInterface, LazyObjectInterface
{
+ use BgsaveTrait;
use CopyTrait;
use GeosearchTrait;
use GetrangeTrait;
@@ -338,11 +340,6 @@ public function lcs($key1, $key2, $options = null): mixed
return $this->initializeLazyObject()->lcs(...\func_get_args());
}
- public function bgsave($schedule = false): \Relay\Relay|bool
- {
- return $this->initializeLazyObject()->bgsave(...\func_get_args());
- }
-
public function save(): \Relay\Relay|bool
{
return $this->initializeLazyObject()->save(...\func_get_args());
diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json
index c89d667288286..d56cec522a60c 100644
--- a/src/Symfony/Component/Cache/composer.json
+++ b/src/Symfony/Component/Cache/composer.json
@@ -27,20 +27,20 @@
"symfony/cache-contracts": "^3.6",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
- "symfony/clock": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/filesystem": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"conflict": {
"doctrine/dbal": "<3.6",
diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json
index 37206042aa8b0..af999bafa38ff 100644
--- a/src/Symfony/Component/Config/composer.json
+++ b/src/Symfony/Component/Config/composer.json
@@ -18,15 +18,15 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/filesystem": "^7.1",
+ "symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^6.4|^7.0"
+ "symfony/yaml": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/finder": "<6.4",
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index b4539fa1eeb50..fa3c381cfa233 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Command\DumpCompletionCommand;
@@ -28,6 +29,7 @@
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
@@ -65,7 +67,7 @@
* Usage:
*
* $app = new Application('myapp', '1.0 (stable)');
- * $app->add(new SimpleCommand());
+ * $app->addCommand(new SimpleCommand());
* $app->run();
*
* @author Fabien Potencier
@@ -512,7 +514,7 @@ public function getLongVersion(): string
*/
public function register(string $name): Command
{
- return $this->add(new Command($name));
+ return $this->addCommand(new Command($name));
}
/**
@@ -520,25 +522,39 @@ public function register(string $name): Command
*
* If a Command is not enabled it will not be added.
*
- * @param Command[] $commands An array of commands
+ * @param callable[]|Command[] $commands An array of commands
*/
public function addCommands(array $commands): void
{
foreach ($commands as $command) {
- $this->add($command);
+ $this->addCommand($command);
}
}
+ /**
+ * @deprecated since Symfony 7.4, use Application::addCommand() instead
+ */
+ public function add(Command $command): ?Command
+ {
+ trigger_deprecation('symfony/console', '7.4', 'The "%s()" method is deprecated and will be removed in Symfony 8.0, use "%s::addCommand()" instead.', __METHOD__, self::class);
+
+ return $this->addCommand($command);
+ }
+
/**
* Adds a command object.
*
* If a command with the same name already exists, it will be overridden.
* If the command is not enabled it will not be added.
*/
- public function add(Command $command): ?Command
+ public function addCommand(callable|Command $command): ?Command
{
$this->init();
+ if (!$command instanceof Command) {
+ $command = new Command(null, $command);
+ }
+
$command->setApplication($this);
if (!$command->isEnabled()) {
@@ -604,7 +620,7 @@ public function has(string $name): bool
{
$this->init();
- return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name)));
+ return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->addCommand($this->commandLoader->get($name)));
}
/**
@@ -1321,8 +1337,14 @@ private function init(): void
}
$this->initialized = true;
+ if ((new \ReflectionMethod($this, 'add'))->getDeclaringClass()->getName() !== (new \ReflectionMethod($this, 'addCommand'))->getDeclaringClass()->getName()) {
+ $adder = $this->add(...);
+ } else {
+ $adder = $this->addCommand(...);
+ }
+
foreach ($this->getDefaultCommands() as $command) {
- $this->add($command);
+ $adder($command);
}
}
}
diff --git a/src/Symfony/Component/Console/Attribute/Argument.php b/src/Symfony/Component/Console/Attribute/Argument.php
index e6a94d2f10e4c..f2c813d3b1a0f 100644
--- a/src/Symfony/Component/Console/Attribute/Argument.php
+++ b/src/Symfony/Component/Console/Attribute/Argument.php
@@ -13,6 +13,7 @@
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -27,6 +28,7 @@ class Argument
private array|\Closure $suggestedValues;
private ?int $mode = null;
private string $function = '';
+ private string $typeName = '';
/**
* Represents a console command definition.
@@ -66,20 +68,23 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function));
}
- $parameterTypeName = $type->getName();
+ $self->typeName = $type->getName();
+ $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
- if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) {
- throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
+ if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
+ throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
}
if (!$self->name) {
$self->name = (new UnicodeString($name))->kebab();
}
- $self->default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
+ if ($parameter->isDefaultValueAvailable()) {
+ $self->default = $parameter->getDefaultValue() instanceof \BackedEnum ? $parameter->getDefaultValue()->value : $parameter->getDefaultValue();
+ }
$self->mode = $parameter->isDefaultValueAvailable() || $parameter->allowsNull() ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
- if ('array' === $parameterTypeName) {
+ if ('array' === $self->typeName) {
$self->mode |= InputArgument::IS_ARRAY;
}
@@ -87,6 +92,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
}
+ if ($isBackedEnum && !$self->suggestedValues) {
+ $self->suggestedValues = array_column(($self->typeName)::cases(), 'value');
+ }
+
return $self;
}
@@ -105,6 +114,12 @@ public function toInputArgument(): InputArgument
*/
public function resolveValue(InputInterface $input): mixed
{
- return $input->getArgument($this->name);
+ $value = $input->getArgument($this->name);
+
+ if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) {
+ return ($this->typeName)::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues);
+ }
+
+ return $value;
}
}
diff --git a/src/Symfony/Component/Console/Attribute/AsCommand.php b/src/Symfony/Component/Console/Attribute/AsCommand.php
index 767d46ebb7ff1..02f1562012d7f 100644
--- a/src/Symfony/Component/Console/Attribute/AsCommand.php
+++ b/src/Symfony/Component/Console/Attribute/AsCommand.php
@@ -25,6 +25,7 @@ class AsCommand
* @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
* @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
* @param string|null $help The help content of the command, displayed with the help page
+ * @param string[] $usages The list of usage examples, displayed with the help page
*/
public function __construct(
public string $name,
@@ -32,6 +33,7 @@ public function __construct(
array $aliases = [],
bool $hidden = false,
public ?string $help = null,
+ public array $usages = [],
) {
if (!$hidden && !$aliases) {
return;
diff --git a/src/Symfony/Component/Console/Attribute/Option.php b/src/Symfony/Component/Console/Attribute/Option.php
index 2f0256b177658..8065d6ad82ed8 100644
--- a/src/Symfony/Component/Console/Attribute/Option.php
+++ b/src/Symfony/Component/Console/Attribute/Option.php
@@ -13,6 +13,7 @@
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -75,7 +76,7 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
$self->name = (new UnicodeString($name))->kebab();
}
- $self->default = $parameter->getDefaultValue();
+ $self->default = $parameter->getDefaultValue() instanceof \BackedEnum ? $parameter->getDefaultValue()->value : $parameter->getDefaultValue();
$self->allowNull = $parameter->allowsNull();
if ($type instanceof \ReflectionUnionType) {
@@ -87,9 +88,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
}
$self->typeName = $type->getName();
+ $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
- if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
- throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
+ if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
+ throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types and BackedEnum are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
}
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
@@ -115,6 +117,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
}
+ if ($isBackedEnum && !$self->suggestedValues) {
+ $self->suggestedValues = array_column(($self->typeName)::cases(), 'value');
+ }
+
return $self;
}
@@ -140,6 +146,10 @@ public function resolveValue(InputInterface $input): mixed
return true;
}
+ if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) {
+ return ($this->typeName)::tryFrom($value) ?? throw InvalidOptionException::fromEnumValue($this->name, $value, $this->suggestedValues);
+ }
+
if ('array' === $this->typeName && $this->allowNull && [] === $value) {
return null;
}
diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md
index 9f3ae3d7d2326..722045091ff49 100644
--- a/src/Symfony/Component/Console/CHANGELOG.md
+++ b/src/Symfony/Component/Console/CHANGELOG.md
@@ -1,6 +1,16 @@
CHANGELOG
=========
+7.4
+---
+
+ * Allow setting aliases and the hidden flag via the command name passed to the constructor
+ * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone
+ * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()`
+ * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands
+ * Allow Usages to be specified via `#[AsCommand]` attribute.
+ * Allow passing invokable commands to `Symfony\Component\Console\Tester\CommandTester`
+
7.3
---
diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php
index f6cd8499791f1..9e6e41ec9cda5 100644
--- a/src/Symfony/Component/Console/Command/Command.php
+++ b/src/Symfony/Component/Console/Command/Command.php
@@ -87,23 +87,44 @@ public static function getDefaultDescription(): ?string
*
* @throws LogicException When the command name is empty
*/
- public function __construct(?string $name = null)
+ public function __construct(?string $name = null, ?callable $code = null)
{
$this->definition = new InputDefinition();
+ if ($code !== null) {
+ if (!\is_object($code) || $code instanceof \Closure) {
+ throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class));
+ }
+
+ /** @var AsCommand $attribute */
+ $attribute = ((new \ReflectionObject($code))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance()
+ ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class));
+
+ $this->setName($name ?? $attribute->name)
+ ->setDescription($attribute->description ?? '')
+ ->setHelp($attribute->help ?? '')
+ ->setCode($code);
+
+ foreach ($attribute->usages as $usage) {
+ $this->addUsage($usage);
+ }
+
+ return;
+ }
+
$attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
if (null === $name) {
if (self::class !== (new \ReflectionMethod($this, 'getDefaultName'))->class) {
trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class);
- $defaultName = static::getDefaultName();
+ $name = static::getDefaultName();
} else {
- $defaultName = $attribute?->name;
+ $name = $attribute?->name;
}
}
- if (null === $name && null !== $name = $defaultName) {
+ if (null !== $name) {
$aliases = explode('|', $name);
if ('' === $name = array_shift($aliases)) {
@@ -134,6 +155,10 @@ public function __construct(?string $name = null)
$this->setHelp($attribute?->help ?? '');
}
+ foreach ($attribute?->usages ?? [] as $usage) {
+ $this->addUsage($usage);
+ }
+
if (\is_callable($this) && (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name === self::class) {
$this->code = new InvokableCommand($this, $this(...));
}
diff --git a/src/Symfony/Component/Console/Debug/CliRequest.php b/src/Symfony/Component/Console/Debug/CliRequest.php
index b023db07af95e..6e2c1012b16ef 100644
--- a/src/Symfony/Component/Console/Debug/CliRequest.php
+++ b/src/Symfony/Component/Console/Debug/CliRequest.php
@@ -24,7 +24,7 @@ public function __construct(
public readonly TraceableCommand $command,
) {
parent::__construct(
- attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'],
+ attributes: ['_controller' => $command->command::class, '_virtual_type' => 'command'],
server: $_SERVER,
);
}
diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php
index 562627f4b6114..4a0ee42296032 100644
--- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php
+++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php
@@ -91,6 +91,7 @@ public function process(ContainerBuilder $container): void
$description = $tags[0]['description'] ?? null;
$help = $tags[0]['help'] ?? null;
+ $usages = $tags[0]['usages'] ?? null;
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
@@ -108,6 +109,7 @@ public function process(ContainerBuilder $container): void
$description ??= $tag['description'] ?? null;
$help ??= $tag['help'] ?? null;
+ $usages ??= $tag['usages'] ?? null;
}
$definition->addMethodCall('setName', [$commandName]);
@@ -124,6 +126,12 @@ public function process(ContainerBuilder $container): void
$definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]);
}
+ if ($usages) {
+ foreach ($usages as $usage) {
+ $definition->addMethodCall('addUsage', [$usage]);
+ }
+ }
+
if (!$description) {
if (Command::class !== (new \ReflectionMethod($class, 'getDefaultDescription'))->class) {
trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class);
diff --git a/src/Symfony/Component/Console/Exception/InvalidArgumentException.php b/src/Symfony/Component/Console/Exception/InvalidArgumentException.php
index 07cc0b61d6dc8..0482244f2066b 100644
--- a/src/Symfony/Component/Console/Exception/InvalidArgumentException.php
+++ b/src/Symfony/Component/Console/Exception/InvalidArgumentException.php
@@ -16,4 +16,17 @@
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
+ /**
+ * @internal
+ */
+ public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self
+ {
+ $error = \sprintf('The value "%s" is not valid for the "%s" argument.', $value, $name);
+
+ if (\is_array($suggestedValues)) {
+ $error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues));
+ }
+
+ return new self($error);
+ }
}
diff --git a/src/Symfony/Component/Console/Exception/InvalidOptionException.php b/src/Symfony/Component/Console/Exception/InvalidOptionException.php
index 5cf62792e43c8..e59167df12fe9 100644
--- a/src/Symfony/Component/Console/Exception/InvalidOptionException.php
+++ b/src/Symfony/Component/Console/Exception/InvalidOptionException.php
@@ -18,4 +18,17 @@
*/
class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface
{
+ /**
+ * @internal
+ */
+ public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self
+ {
+ $error = \sprintf('The value "%s" is not valid for the "%s" option.', $value, $name);
+
+ if (\is_array($suggestedValues)) {
+ $error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues));
+ }
+
+ return new self($error);
+ }
}
diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php
index 8e1591ec1b14a..9b65c321368fe 100644
--- a/src/Symfony/Component/Console/Helper/QuestionHelper.php
+++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php
@@ -234,7 +234,8 @@ protected function writeError(OutputInterface $output, \Exception $error): void
/**
* Autocompletes a question.
*
- * @param resource $inputStream
+ * @param resource $inputStream
+ * @param callable(string):string[] $autocomplete
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php
index 46a60c798b0ba..cb65bd6746ee0 100644
--- a/src/Symfony/Component/Console/Question/Question.php
+++ b/src/Symfony/Component/Console/Question/Question.php
@@ -24,8 +24,17 @@ class Question
private ?int $attempts = null;
private bool $hidden = false;
private bool $hiddenFallback = true;
+ /**
+ * @var (\Closure(string):string[])|null
+ */
private ?\Closure $autocompleterCallback = null;
+ /**
+ * @var (\Closure(mixed):mixed)|null
+ */
private ?\Closure $validator = null;
+ /**
+ * @var (\Closure(mixed):mixed)|null
+ */
private ?\Closure $normalizer = null;
private bool $trimmable = true;
private bool $multiline = false;
@@ -160,6 +169,8 @@ public function setAutocompleterValues(?iterable $values): static
/**
* Gets the callback function used for the autocompleter.
+ *
+ * @return (callable(string):string[])|null
*/
public function getAutocompleterCallback(): ?callable
{
@@ -171,6 +182,8 @@ public function getAutocompleterCallback(): ?callable
*
* The callback is passed the user input as argument and should return an iterable of corresponding suggestions.
*
+ * @param (callable(string):string[])|null $callback
+ *
* @return $this
*/
public function setAutocompleterCallback(?callable $callback): static
@@ -187,6 +200,8 @@ public function setAutocompleterCallback(?callable $callback): static
/**
* Sets a validator for the question.
*
+ * @param (callable(mixed):mixed)|null $validator
+ *
* @return $this
*/
public function setValidator(?callable $validator): static
@@ -198,6 +213,8 @@ public function setValidator(?callable $validator): static
/**
* Gets the validator for the question.
+ *
+ * @return (callable(mixed):mixed)|null
*/
public function getValidator(): ?callable
{
@@ -237,7 +254,7 @@ public function getMaxAttempts(): ?int
/**
* Sets a normalizer for the response.
*
- * The normalizer can be a callable (a string), a closure or a class implementing __invoke.
+ * @param callable(mixed):mixed $normalizer
*
* @return $this
*/
@@ -251,7 +268,7 @@ public function setNormalizer(callable $normalizer): static
/**
* Gets the normalizer for the response.
*
- * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
+ * @return (callable(mixed):mixed)|null
*/
public function getNormalizer(): ?callable
{
diff --git a/src/Symfony/Component/Console/SingleCommandApplication.php b/src/Symfony/Component/Console/SingleCommandApplication.php
index 2b54fb870d244..837948d1287b1 100644
--- a/src/Symfony/Component/Console/SingleCommandApplication.php
+++ b/src/Symfony/Component/Console/SingleCommandApplication.php
@@ -57,7 +57,7 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu
$application->setAutoExit($this->autoExit);
// Fix the usage of the command displayed with "--help"
$this->setName($_SERVER['argv'][0]);
- $application->add($this);
+ $application->addCommand($this);
$application->setDefaultCommand($this->getName(), true);
$this->running = true;
diff --git a/src/Symfony/Component/Console/Style/StyleInterface.php b/src/Symfony/Component/Console/Style/StyleInterface.php
index fcc5bc775f8a9..1a2232324aef2 100644
--- a/src/Symfony/Component/Console/Style/StyleInterface.php
+++ b/src/Symfony/Component/Console/Style/StyleInterface.php
@@ -70,11 +70,15 @@ public function table(array $headers, array $rows): void;
/**
* Asks a question.
+ *
+ * @param (callable(mixed):mixed)|null $validator
*/
public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;
/**
* Asks a question with the user input hidden.
+ *
+ * @param (callable(mixed):mixed)|null $validator
*/
public function askHidden(string $question, ?callable $validator = null): mixed;
diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php
index d39cde7f6e8e2..714d88ad51dea 100644
--- a/src/Symfony/Component/Console/Tester/CommandTester.php
+++ b/src/Symfony/Component/Console/Tester/CommandTester.php
@@ -24,9 +24,12 @@ class CommandTester
{
use TesterTrait;
+ private Command $command;
+
public function __construct(
- private Command $command,
+ callable|Command $command,
) {
+ $this->command = $command instanceof Command ? $command : new Command(null, $command);
}
/**
diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php
index 268f8ba501a9e..e9b45c051dc0f 100644
--- a/src/Symfony/Component/Console/Tests/ApplicationTest.php
+++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
+use Symfony\Component\Console\Command\InvokableCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
@@ -28,6 +29,8 @@
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
@@ -45,6 +48,8 @@
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
use Symfony\Component\Console\Terminal;
use Symfony\Component\Console\Tester\ApplicationTester;
+use Symfony\Component\Console\Tests\Fixtures\InvokableExtendingCommandTestCommand;
+use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand;
use Symfony\Component\Console\Tests\Fixtures\MockableAppliationWithTerminalWidth;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -163,7 +168,7 @@ public function testAll()
$commands = $application->all();
$this->assertInstanceOf(HelpCommand::class, $commands['help'], '->all() returns the registered commands');
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$commands = $application->all('foo');
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
}
@@ -174,7 +179,7 @@ public function testAllWithCommandLoader()
$commands = $application->all();
$this->assertInstanceOf(HelpCommand::class, $commands['help'], '->all() returns the registered commands');
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$commands = $application->all('foo');
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
@@ -221,7 +226,7 @@ public function testRegisterAmbiguous()
public function testAdd()
{
$application = new Application();
- $application->add($foo = new \FooCommand());
+ $application->addCommand($foo = new \FooCommand());
$commands = $application->all();
$this->assertEquals($foo, $commands['foo:bar'], '->add() registers a command');
@@ -236,7 +241,60 @@ public function testAddCommandWithEmptyConstructor()
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.');
- (new Application())->add(new \Foo5Command());
+ (new Application())->addCommand(new \Foo5Command());
+ }
+
+ public function testAddCommandWithExtendedCommand()
+ {
+ $application = new Application();
+ $application->addCommand($foo = new \FooCommand());
+ $commands = $application->all();
+
+ $this->assertEquals($foo, $commands['foo:bar']);
+ }
+
+ public function testAddCommandWithInvokableCommand()
+ {
+ $application = new Application();
+ $application->addCommand($foo = new InvokableTestCommand());
+ $commands = $application->all();
+
+ $this->assertInstanceOf(Command::class, $command = $commands['invokable:test']);
+ $this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command));
+ }
+
+ public function testAddCommandWithInvokableExtendedCommand()
+ {
+ $application = new Application();
+ $application->addCommand($foo = new InvokableExtendingCommandTestCommand());
+ $commands = $application->all();
+
+ $this->assertEquals($foo, $commands['invokable:test']);
+ }
+
+ /**
+ * @dataProvider provideInvalidInvokableCommands
+ */
+ public function testAddCommandThrowsExceptionOnInvalidCommand(callable $command, string $expectedException, string $expectedExceptionMessage)
+ {
+ $application = new Application();
+
+ $this->expectException($expectedException);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ $application->addCommand($command);
+ }
+
+ public static function provideInvalidInvokableCommands(): iterable
+ {
+ yield 'a function' => ['strlen', InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
+ yield 'a closure' => [function () {
+ }, InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
+ yield 'without the #[AsCommand] attribute' => [new class {
+ public function __invoke()
+ {
+ }
+ }, LogicException::class, \sprintf('The command must use the "%s" attribute.', AsCommand::class)];
}
public function testHasGet()
@@ -245,13 +303,13 @@ public function testHasGet()
$this->assertTrue($application->has('list'), '->has() returns true if a named command is registered');
$this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered');
- $application->add($foo = new \FooCommand());
+ $application->addCommand($foo = new \FooCommand());
$this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered');
$this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name');
$this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
$application = new Application();
- $application->add($foo = new \FooCommand());
+ $application->addCommand($foo = new \FooCommand());
// simulate --help
$r = new \ReflectionObject($application);
$p = $r->getProperty('wantHelps');
@@ -266,7 +324,7 @@ public function testHasGetWithCommandLoader()
$this->assertTrue($application->has('list'), '->has() returns true if a named command is registered');
$this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered');
- $application->add($foo = new \FooCommand());
+ $application->addCommand($foo = new \FooCommand());
$this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered');
$this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name');
$this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
@@ -307,35 +365,35 @@ public function testGetInvalidCommand()
public function testGetNamespaces()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
$this->assertEquals(['foo'], $application->getNamespaces(), '->getNamespaces() returns an array of unique used namespaces');
}
public function testFindNamespace()
{
$application = new Application();
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns the given namespace if it exists');
$this->assertEquals('foo', $application->findNamespace('f'), '->findNamespace() finds a namespace given an abbreviation');
- $application->add(new \Foo2Command());
+ $application->addCommand(new \Foo2Command());
$this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns the given namespace if it exists');
}
public function testFindNamespaceWithSubnamespaces()
{
$application = new Application();
- $application->add(new \FooSubnamespaced1Command());
- $application->add(new \FooSubnamespaced2Command());
+ $application->addCommand(new \FooSubnamespaced1Command());
+ $application->addCommand(new \FooSubnamespaced2Command());
$this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns commands even if the commands are only contained in subnamespaces');
}
public function testFindAmbiguousNamespace()
{
$application = new Application();
- $application->add(new \BarBucCommand());
- $application->add(new \FooCommand());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \BarBucCommand());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo2Command());
$expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1";
@@ -348,8 +406,8 @@ public function testFindAmbiguousNamespace()
public function testFindNonAmbiguous()
{
$application = new Application();
- $application->add(new \TestAmbiguousCommandRegistering());
- $application->add(new \TestAmbiguousCommandRegistering2());
+ $application->addCommand(new \TestAmbiguousCommandRegistering());
+ $application->addCommand(new \TestAmbiguousCommandRegistering2());
$this->assertEquals('test-ambiguous', $application->find('test')->getName());
}
@@ -364,9 +422,9 @@ public function testFindInvalidNamespace()
public function testFindUniqueNameButNamespaceName()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('Command "foo1" is not defined');
@@ -377,7 +435,7 @@ public function testFindUniqueNameButNamespaceName()
public function testFind()
{
$application = new Application();
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$this->assertInstanceOf(\FooCommand::class, $application->find('foo:bar'), '->find() returns a command if its name exists');
$this->assertInstanceOf(HelpCommand::class, $application->find('h'), '->find() returns a command if its name exists');
@@ -389,8 +447,8 @@ public function testFind()
public function testFindCaseSensitiveFirst()
{
$application = new Application();
- $application->add(new \FooSameCaseUppercaseCommand());
- $application->add(new \FooSameCaseLowercaseCommand());
+ $application->addCommand(new \FooSameCaseUppercaseCommand());
+ $application->addCommand(new \FooSameCaseLowercaseCommand());
$this->assertInstanceOf(\FooSameCaseUppercaseCommand::class, $application->find('f:B'), '->find() returns a command if the abbreviation is the correct case');
$this->assertInstanceOf(\FooSameCaseUppercaseCommand::class, $application->find('f:BAR'), '->find() returns a command if the abbreviation is the correct case');
@@ -401,7 +459,7 @@ public function testFindCaseSensitiveFirst()
public function testFindCaseInsensitiveAsFallback()
{
$application = new Application();
- $application->add(new \FooSameCaseLowercaseCommand());
+ $application->addCommand(new \FooSameCaseLowercaseCommand());
$this->assertInstanceOf(\FooSameCaseLowercaseCommand::class, $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case');
$this->assertInstanceOf(\FooSameCaseLowercaseCommand::class, $application->find('f:B'), '->find() will fallback to case insensitivity');
@@ -411,8 +469,8 @@ public function testFindCaseInsensitiveAsFallback()
public function testFindCaseInsensitiveSuggestions()
{
$application = new Application();
- $application->add(new \FooSameCaseLowercaseCommand());
- $application->add(new \FooSameCaseUppercaseCommand());
+ $application->addCommand(new \FooSameCaseLowercaseCommand());
+ $application->addCommand(new \FooSameCaseUppercaseCommand());
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('Command "FoO:BaR" is ambiguous');
@@ -444,9 +502,9 @@ public function testFindWithAmbiguousAbbreviations($abbreviation, $expectedExcep
$this->expectExceptionMessage($expectedExceptionMessage);
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
$application->find($abbreviation);
}
@@ -476,8 +534,8 @@ public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreH
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \FooHiddenCommand());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \FooHiddenCommand());
$this->assertInstanceOf(\FooCommand::class, $application->find('foo:'));
}
@@ -485,8 +543,8 @@ public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreH
public function testFindCommandEqualNamespace()
{
$application = new Application();
- $application->add(new \Foo3Command());
- $application->add(new \Foo4Command());
+ $application->addCommand(new \Foo3Command());
+ $application->addCommand(new \Foo4Command());
$this->assertInstanceOf(\Foo3Command::class, $application->find('foo3:bar'), '->find() returns the good command even if a namespace has same name');
$this->assertInstanceOf(\Foo4Command::class, $application->find('foo3:bar:toh'), '->find() returns a command even if its namespace equals another command name');
@@ -495,8 +553,8 @@ public function testFindCommandEqualNamespace()
public function testFindCommandWithAmbiguousNamespacesButUniqueName()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \FoobarCommand());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \FoobarCommand());
$this->assertInstanceOf(\FoobarCommand::class, $application->find('f:f'));
}
@@ -504,7 +562,7 @@ public function testFindCommandWithAmbiguousNamespacesButUniqueName()
public function testFindCommandWithMissingNamespace()
{
$application = new Application();
- $application->add(new \Foo4Command());
+ $application->addCommand(new \Foo4Command());
$this->assertInstanceOf(\Foo4Command::class, $application->find('f::t'));
}
@@ -515,7 +573,7 @@ public function testFindCommandWithMissingNamespace()
public function testFindAlternativeExceptionMessageSingle($name)
{
$application = new Application();
- $application->add(new \Foo3Command());
+ $application->addCommand(new \Foo3Command());
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('Did you mean this');
@@ -526,7 +584,7 @@ public function testFindAlternativeExceptionMessageSingle($name)
public function testDontRunAlternativeNamespaceName()
{
$application = new Application();
- $application->add(new \Foo1Command());
+ $application->addCommand(new \Foo1Command());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foos:bar1'], ['decorated' => false]);
@@ -536,7 +594,7 @@ public function testDontRunAlternativeNamespaceName()
public function testCanRunAlternativeCommandName()
{
$application = new Application();
- $application->add(new \FooWithoutAliasCommand());
+ $application->addCommand(new \FooWithoutAliasCommand());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->setInputs(['y']);
@@ -550,7 +608,7 @@ public function testCanRunAlternativeCommandName()
public function testDontRunAlternativeCommandName()
{
$application = new Application();
- $application->add(new \FooWithoutAliasCommand());
+ $application->addCommand(new \FooWithoutAliasCommand());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->setInputs(['n']);
@@ -574,9 +632,9 @@ public function testRunNamespace()
putenv('COLUMNS=120');
$application = new Application();
$application->setAutoExit(false);
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo'], ['decorated' => false]);
$display = trim($tester->getDisplay(true));
@@ -589,9 +647,9 @@ public function testFindAlternativeExceptionMessageMultiple()
{
putenv('COLUMNS=120');
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
// Command + plural
try {
@@ -614,8 +672,8 @@ public function testFindAlternativeExceptionMessageMultiple()
$this->assertMatchesRegularExpression('/foo1/', $e->getMessage());
}
- $application->add(new \Foo3Command());
- $application->add(new \Foo4Command());
+ $application->addCommand(new \Foo3Command());
+ $application->addCommand(new \Foo4Command());
// Subnamespace + plural
try {
@@ -632,9 +690,9 @@ public function testFindAlternativeCommands()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
try {
$application->find($commandName = 'Unknown command');
@@ -669,7 +727,7 @@ public function testFindAlternativeCommandsWithAnAlias()
$application->setCommandLoader(new FactoryCommandLoader([
'foo3' => static fn () => $fooCommand,
]));
- $application->add($fooCommand);
+ $application->addCommand($fooCommand);
$result = $application->find('foo');
@@ -680,10 +738,10 @@ public function testFindAlternativeNamespace()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
- $application->add(new \Foo3Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
+ $application->addCommand(new \Foo3Command());
try {
$application->find('Unknown-namespace:Unknown-command');
@@ -715,11 +773,11 @@ public function testFindAlternativesOutput()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo1Command());
- $application->add(new \Foo2Command());
- $application->add(new \Foo3Command());
- $application->add(new \FooHiddenCommand());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo1Command());
+ $application->addCommand(new \Foo2Command());
+ $application->addCommand(new \Foo3Command());
+ $application->addCommand(new \FooHiddenCommand());
$expectedAlternatives = [
'afoobar',
@@ -755,8 +813,8 @@ public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces()
public function testFindWithDoubleColonInNameThrowsException()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \Foo4Command());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \Foo4Command());
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('Command "foo::bar" is not defined.');
@@ -767,7 +825,7 @@ public function testFindWithDoubleColonInNameThrowsException()
public function testFindHiddenWithExactName()
{
$application = new Application();
- $application->add(new \FooHiddenCommand());
+ $application->addCommand(new \FooHiddenCommand());
$this->assertInstanceOf(\FooHiddenCommand::class, $application->find('foo:hidden'));
$this->assertInstanceOf(\FooHiddenCommand::class, $application->find('afoohidden'));
@@ -777,8 +835,8 @@ public function testFindAmbiguousCommandsIfAllAlternativesAreHidden()
{
$application = new Application();
- $application->add(new \FooCommand());
- $application->add(new \FooHiddenCommand());
+ $application->addCommand(new \FooCommand());
+ $application->addCommand(new \FooHiddenCommand());
$this->assertInstanceOf(\FooCommand::class, $application->find('foo:'));
}
@@ -824,7 +882,7 @@ public function testSetCatchErrors(bool $catchExceptions)
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions($catchExceptions);
- $application->add((new Command('boom'))->setCode(fn () => throw new \Error('This is an error.')));
+ $application->addCommand((new Command('boom'))->setCode(fn () => throw new \Error('This is an error.')));
putenv('COLUMNS=120');
$tester = new ApplicationTester($application);
@@ -870,7 +928,7 @@ public function testRenderException()
$tester->run(['command' => 'list', '--foo' => true], ['decorated' => false, 'capture_stderr_separately' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command');
- $application->add(new \Foo3Command());
+ $application->addCommand(new \Foo3Command());
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo3:bar'], ['decorated' => false, 'capture_stderr_separately' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
@@ -1031,7 +1089,7 @@ public function testRun()
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
- $application->add($command = new \Foo1Command());
+ $application->addCommand($command = new \Foo1Command());
$_SERVER['argv'] = ['cli.php', 'foo:bar1'];
ob_start();
@@ -1116,7 +1174,7 @@ public function testRun()
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo:bar', '--no-interaction' => true], ['decorated' => false]);
@@ -1151,7 +1209,7 @@ public function testVerboseValueNotBreakArguments()
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$output = new StreamOutput(fopen('php://memory', 'w', false));
@@ -1762,7 +1820,7 @@ public function testSetRunCustomDefaultCommand()
$application = new Application();
$application->setAutoExit(false);
- $application->add($command);
+ $application->addCommand($command);
$application->setDefaultCommand($command->getName());
$tester = new ApplicationTester($application);
@@ -1784,7 +1842,7 @@ public function testSetRunCustomDefaultCommandWithOption()
$application = new Application();
$application->setAutoExit(false);
- $application->add($command);
+ $application->addCommand($command);
$application->setDefaultCommand($command->getName());
$tester = new ApplicationTester($application);
@@ -1799,7 +1857,7 @@ public function testSetRunCustomSingleCommand()
$application = new Application();
$application->setAutoExit(false);
- $application->add($command);
+ $application->addCommand($command);
$application->setDefaultCommand($command->getName(), true);
$tester = new ApplicationTester($application);
@@ -2150,7 +2208,7 @@ public function testSignalableCommandInterfaceWithoutSignals()
$application = new Application();
$application->setAutoExit(false);
$application->setDispatcher($dispatcher);
- $application->add($command);
+ $application->addCommand($command);
$this->assertSame(0, $application->run(new ArrayInput(['signal'])));
}
@@ -2186,7 +2244,7 @@ public function testSignalableCommandDoesNotInterruptedOnTermSignals()
$application = new Application();
$application->setAutoExit(false);
$application->setDispatcher($dispatcher);
- $application->add($command);
+ $application->addCommand($command);
$this->assertSame(129, $application->run(new ArrayInput(['signal'])));
}
@@ -2208,7 +2266,7 @@ public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals()
$application = new Application();
$application->setAutoExit(false);
$application->setDispatcher($dispatcher);
- $application->add($command);
+ $application->addCommand($command);
$tester = new ApplicationTester($application);
$this->assertSame(51, $tester->run(['signal']));
$expected = <<setAutoExit(false);
$application->setDispatcher($dispatcher);
- $application->add($command);
+ $application->addCommand($command);
$this->assertSame(0, $application->run(new ArrayInput(['alarm'])));
$this->assertFalse($command->signaled);
@@ -2459,7 +2517,7 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI
if ($dispatcher) {
$application->setDispatcher($dispatcher);
}
- $application->add(new LazyCommand($command->getName(), [], '', false, fn () => $command, true));
+ $application->addCommand(new LazyCommand($command->getName(), [], '', false, fn () => $command, true));
return $application;
}
@@ -2491,7 +2549,7 @@ public function __construct()
parent::__construct();
$command = new \FooCommand();
- $this->add($command);
+ $this->addCommand($command);
$this->setDefaultCommand($command->getName());
}
}
diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php
index 0db3572fc3476..a4a719b3d10ab 100644
--- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php
@@ -27,6 +27,7 @@
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand;
class CommandTest extends TestCase
{
@@ -50,7 +51,7 @@ public function testCommandNameCannotBeEmpty()
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The command defined in "Symfony\Component\Console\Command\Command" cannot have an empty name.');
- (new Application())->add(new Command());
+ (new Application())->addCommand(new Command());
}
public function testSetApplication()
@@ -190,7 +191,7 @@ public function testGetProcessedHelp()
$command = new \TestCommand();
$command->setHelp('The %command.name% command does... Example: %command.full_name%.');
$application = new Application();
- $application->add($command);
+ $application->addCommand($command);
$application->setDefaultCommand('namespace:name', true);
$this->assertStringContainsString('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly in single command applications');
$this->assertStringNotContainsString('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name% in single command applications');
@@ -205,6 +206,19 @@ public function testGetSetAliases()
$this->assertEquals(['name1'], $command->getAliases(), '->setAliases() sets the aliases');
}
+ /**
+ * @testWith ["name|alias1|alias2", "name", ["alias1", "alias2"], false]
+ * ["|alias1|alias2", "alias1", ["alias2"], true]
+ */
+ public function testSetAliasesAndHiddenViaName(string $name, string $expectedName, array $expectedAliases, bool $expectedHidden)
+ {
+ $command = new Command($name);
+
+ self::assertSame($expectedName, $command->getName());
+ self::assertSame($expectedHidden, $command->isHidden());
+ self::assertSame($expectedAliases, $command->getAliases());
+ }
+
public function testGetSynopsis()
{
$command = new \TestCommand();
@@ -291,6 +305,13 @@ public function testRunInteractive()
$this->assertEquals('interact called'.\PHP_EOL.'execute called'.\PHP_EOL, $tester->getDisplay(), '->run() calls the interact() method if the input is interactive');
}
+ public function testInvokableCommand()
+ {
+ $tester = new CommandTester(new InvokableTestCommand());
+
+ $this->assertSame(Command::SUCCESS, $tester->execute([]));
+ }
+
public function testRunNonInteractive()
{
$tester = new CommandTester(new \TestCommand());
@@ -444,6 +465,8 @@ public function testCommandAttribute()
$this->assertSame('foo', $command->getName());
$this->assertSame('desc', $command->getDescription());
$this->assertSame('help', $command->getHelp());
+ $this->assertCount(2, $command->getUsages());
+ $this->assertStringContainsString('usage1', $command->getUsages()[0]);
$this->assertTrue($command->isHidden());
$this->assertSame(['f'], $command->getAliases());
}
@@ -529,7 +552,7 @@ function createClosure()
};
}
-#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'], help: 'help')]
+#[AsCommand(name: 'foo', description: 'desc', usages: ['usage1', 'usage2'], hidden: true, aliases: ['f'], help: 'help')]
class Php8Command extends Command
{
}
diff --git a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php
index 75519eb49e5e3..08f6b046ff7e4 100644
--- a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php
@@ -33,7 +33,7 @@ protected function setUp(): void
$this->command = new CompleteCommand();
$this->application = new Application();
- $this->application->add(new CompleteCommandTest_HelloCommand());
+ $this->application->addCommand(new CompleteCommandTest_HelloCommand());
$this->command->setApplication($this->application);
$this->tester = new CommandTester($this->command);
diff --git a/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php b/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php
index c36ab62df02c1..f1979c0dc8475 100644
--- a/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php
@@ -77,7 +77,7 @@ public function testComplete(array $input, array $expectedSuggestions)
{
require_once realpath(__DIR__.'/../Fixtures/FooCommand.php');
$application = new Application();
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$tester = new CommandCompletionTester($application->get('help'));
$suggestions = $tester->complete($input, 2);
$this->assertSame($expectedSuggestions, $suggestions);
diff --git a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php
index 5ab7951e7f575..785891586ca83 100644
--- a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Tests\Command;
+use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
@@ -18,6 +19,7 @@
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\ArrayInput;
@@ -132,6 +134,88 @@ public function testCommandInputOptionDefinition()
self::assertFalse($optInputOption->getDefault());
}
+ public function testEnumArgument()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Argument] StringEnum $enum,
+ #[Argument] StringEnum $enumWithDefault = StringEnum::Image,
+ #[Argument] ?StringEnum $nullableEnum = null,
+ ): int {
+ Assert::assertSame(StringEnum::Image, $enum);
+ Assert::assertSame(StringEnum::Image, $enumWithDefault);
+ Assert::assertNull($nullableEnum);
+
+ return 0;
+ });
+
+ $enumInputArgument = $command->getDefinition()->getArgument('enum');
+ self::assertTrue($enumInputArgument->isRequired());
+ self::assertNull($enumInputArgument->getDefault());
+ self::assertTrue($enumInputArgument->hasCompletion());
+
+ $enumWithDefaultInputArgument = $command->getDefinition()->getArgument('enum-with-default');
+ self::assertFalse($enumWithDefaultInputArgument->isRequired());
+ self::assertSame('image', $enumWithDefaultInputArgument->getDefault());
+ self::assertTrue($enumWithDefaultInputArgument->hasCompletion());
+
+ $nullableEnumInputArgument = $command->getDefinition()->getArgument('nullable-enum');
+ self::assertFalse($nullableEnumInputArgument->isRequired());
+ self::assertNull($nullableEnumInputArgument->getDefault());
+ self::assertTrue($nullableEnumInputArgument->hasCompletion());
+
+ $enumInputArgument->complete(CompletionInput::fromTokens([], 0), $suggestions = new CompletionSuggestions());
+ self::assertEquals([new Suggestion('image'), new Suggestion('video')], $suggestions->getValueSuggestions());
+
+ $command->run(new ArrayInput(['enum' => 'image']), new NullOutput());
+
+ self::expectException(InvalidArgumentException::class);
+ self::expectExceptionMessage('The value "incorrect" is not valid for the "enum" argument. Supported values are "image", "video".');
+
+ $command->run(new ArrayInput(['enum' => 'incorrect']), new NullOutput());
+ }
+
+ public function testEnumOption()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Option] StringEnum $enum = StringEnum::Video,
+ #[Option] StringEnum $enumWithDefault = StringEnum::Image,
+ #[Option] ?StringEnum $nullableEnum = null,
+ ): int {
+ Assert::assertSame(StringEnum::Image, $enum);
+ Assert::assertSame(StringEnum::Image, $enumWithDefault);
+ Assert::assertNull($nullableEnum);
+
+ return 0;
+ });
+
+ $enumInputOption = $command->getDefinition()->getOption('enum');
+ self::assertTrue($enumInputOption->isValueRequired());
+ self::assertSame('video', $enumInputOption->getDefault());
+ self::assertTrue($enumInputOption->hasCompletion());
+
+ $enumWithDefaultInputOption = $command->getDefinition()->getOption('enum-with-default');
+ self::assertTrue($enumWithDefaultInputOption->isValueRequired());
+ self::assertSame('image', $enumWithDefaultInputOption->getDefault());
+ self::assertTrue($enumWithDefaultInputOption->hasCompletion());
+
+ $nullableEnumInputOption = $command->getDefinition()->getOption('nullable-enum');
+ self::assertTrue($nullableEnumInputOption->isValueRequired());
+ self::assertNull($nullableEnumInputOption->getDefault());
+ self::assertTrue($nullableEnumInputOption->hasCompletion());
+
+ $enumInputOption->complete(CompletionInput::fromTokens([], 0), $suggestions = new CompletionSuggestions());
+ self::assertEquals([new Suggestion('image'), new Suggestion('video')], $suggestions->getValueSuggestions());
+
+ $command->run(new ArrayInput(['--enum' => 'image']), new NullOutput());
+
+ self::expectException(InvalidOptionException::class);
+ self::expectExceptionMessage('The value "incorrect" is not valid for the "enum" option. Supported values are "image", "video".');
+
+ $command->run(new ArrayInput(['--enum' => 'incorrect']), new NullOutput());
+ }
+
public function testInvalidArgumentType()
{
$command = new Command('foo');
@@ -377,3 +461,9 @@ public function getSuggestedRoles(CompletionInput $input): array
return ['ROLE_ADMIN', 'ROLE_USER'];
}
}
+
+enum StringEnum: string
+{
+ case Image = 'image';
+ case Video = 'video';
+}
diff --git a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
index a6ffc8ab5bbc9..37496c6b33bb2 100644
--- a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
@@ -54,7 +54,7 @@ public function testExecuteListsCommandsWithNamespaceArgument()
{
require_once realpath(__DIR__.'/../Fixtures/FooCommand.php');
$application = new Application();
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$commandTester = new CommandTester($command = $application->get('list'));
$commandTester->execute(['command' => $command->getName(), 'namespace' => 'foo', '--raw' => true]);
$output = <<<'EOF'
@@ -69,7 +69,7 @@ public function testExecuteListsCommandsOrder()
{
require_once realpath(__DIR__.'/../Fixtures/Foo6Command.php');
$application = new Application();
- $application->add(new \Foo6Command());
+ $application->addCommand(new \Foo6Command());
$commandTester = new CommandTester($command = $application->get('list'));
$commandTester->execute(['command' => $command->getName()], ['decorated' => false]);
$output = <<<'EOF'
@@ -102,7 +102,7 @@ public function testExecuteListsCommandsOrderRaw()
{
require_once realpath(__DIR__.'/../Fixtures/Foo6Command.php');
$application = new Application();
- $application->add(new \Foo6Command());
+ $application->addCommand(new \Foo6Command());
$commandTester = new CommandTester($command = $application->get('list'));
$commandTester->execute(['command' => $command->getName(), '--raw' => true]);
$output = <<<'EOF'
@@ -122,7 +122,7 @@ public function testComplete(array $input, array $expectedSuggestions)
{
require_once realpath(__DIR__.'/../Fixtures/FooCommand.php');
$application = new Application();
- $application->add(new \FooCommand());
+ $application->addCommand(new \FooCommand());
$tester = new CommandCompletionTester($application->get('list'));
$suggestions = $tester->complete($input, 2);
$this->assertSame($expectedSuggestions, $suggestions);
diff --git a/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php
index 408f8c0d35c58..3421eda805251 100644
--- a/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php
+++ b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php
@@ -58,7 +58,7 @@ public function testEventAliases()
->setPublic(true)
->addMethodCall('setAutoExit', [false])
->addMethodCall('setDispatcher', [new Reference('event_dispatcher')])
- ->addMethodCall('add', [new Reference('failing_command')])
+ ->addMethodCall('addCommand', [new Reference('failing_command')])
;
$container->compile();
diff --git a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php
index 9ac660100ea0d..a11e6b5109acb 100644
--- a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php
+++ b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php
@@ -315,6 +315,7 @@ public function testProcessInvokableCommand()
$definition->addTag('console.command', [
'command' => 'invokable',
'description' => 'The command description',
+ 'usages' => ['usage1', 'usage2'],
'help' => 'The %command.name% command help content.',
]);
$container->setDefinition('invokable_command', $definition);
@@ -325,6 +326,8 @@ public function testProcessInvokableCommand()
self::assertTrue($container->has('invokable_command.command'));
self::assertSame('The command description', $command->getDescription());
self::assertSame('The %command.name% command help content.', $command->getHelp());
+ self::assertCount(2, $command->getUsages());
+ $this->assertStringContainsString('usage1', $command->getUsages()[0]);
}
public function testProcessInvokableSignalableCommand()
diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
index 1933c985cbad7..ab90320cd6846 100644
--- a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
+++ b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
@@ -25,7 +25,7 @@ public function testGetNamespaces(array $expected, array $names)
{
$application = new TestApplication();
foreach ($names as $name) {
- $application->add(new Command($name));
+ $application->addCommand(new Command($name));
}
$this->assertSame($expected, array_keys((new ApplicationDescription($application))->getNamespaces()));
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplication2.php b/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplication2.php
index 7bb02fa54c1ff..c755bab383efe 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplication2.php
+++ b/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplication2.php
@@ -18,9 +18,9 @@ class DescriptorApplication2 extends Application
public function __construct()
{
parent::__construct('My Symfony application', 'v1.0');
- $this->add(new DescriptorCommand1());
- $this->add(new DescriptorCommand2());
- $this->add(new DescriptorCommand3());
- $this->add(new DescriptorCommand4());
+ $this->addCommand(new DescriptorCommand1());
+ $this->addCommand(new DescriptorCommand2());
+ $this->addCommand(new DescriptorCommand3());
+ $this->addCommand(new DescriptorCommand4());
}
}
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplicationMbString.php b/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplicationMbString.php
index bf170c449f51e..a76e0e181047f 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplicationMbString.php
+++ b/src/Symfony/Component/Console/Tests/Fixtures/DescriptorApplicationMbString.php
@@ -19,6 +19,6 @@ public function __construct()
{
parent::__construct('MbString åpplicätion');
- $this->add(new DescriptorCommandMbString());
+ $this->addCommand(new DescriptorCommandMbString());
}
}
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php
new file mode 100644
index 0000000000000..724951608c23f
--- /dev/null
+++ b/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php
@@ -0,0 +1,15 @@
+add($command);
+ $application->addCommand($command);
$tester = new CommandTester($application->find('foo'));
@@ -267,4 +269,24 @@ public function testErrorOutput()
$this->assertSame('foo', $tester->getErrorOutput());
}
+
+ public function testAInvokableCommand()
+ {
+ $command = new InvokableTestCommand();
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+
+ $tester->assertCommandIsSuccessful();
+ }
+
+ public function testAInvokableExtendedCommand()
+ {
+ $command = new InvokableExtendingCommandTestCommand();
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+
+ $tester->assertCommandIsSuccessful();
+ }
}
diff --git a/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt b/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt
index c2cf3edc7d1c0..a53af85672709 100644
--- a/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt
+++ b/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt
@@ -53,7 +53,7 @@ class MyCommand extends Command
$app = new Application();
$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher());
-$app->add(new MyCommand('foo'));
+$app->addCommand(new MyCommand('foo'));
$app
->setDefaultCommand('foo', true)
diff --git a/src/Symfony/Component/Console/Tests/phpt/signal/command_exit.phpt b/src/Symfony/Component/Console/Tests/phpt/signal/command_exit.phpt
index e14f80c47afee..e653d65c1a0d6 100644
--- a/src/Symfony/Component/Console/Tests/phpt/signal/command_exit.phpt
+++ b/src/Symfony/Component/Console/Tests/phpt/signal/command_exit.phpt
@@ -45,7 +45,7 @@ class MyCommand extends Command
$app = new Application();
$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher());
-$app->add(new MyCommand('foo'));
+$app->addCommand(new MyCommand('foo'));
$app
->setDefaultCommand('foo', true)
diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json
index 65d69913aa218..109cdd762f625 100644
--- a/src/Symfony/Component/Console/composer.json
+++ b/src/Symfony/Component/Console/composer.json
@@ -20,19 +20,19 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^7.2"
+ "symfony/string": "^7.2|^8.0"
},
"require-dev": {
- "symfony/config": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/lock": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"provide": {
diff --git a/src/Symfony/Component/DependencyInjection/Alias.php b/src/Symfony/Component/DependencyInjection/Alias.php
index 0ec1161f8908d..73d05b46e4185 100644
--- a/src/Symfony/Component/DependencyInjection/Alias.php
+++ b/src/Symfony/Component/DependencyInjection/Alias.php
@@ -103,4 +103,20 @@ public function __toString(): string
{
return $this->id;
}
+
+ public function __serialize(): array
+ {
+ $data = [];
+ foreach ((array) $this as $k => $v) {
+ if (!$v) {
+ continue;
+ }
+ if (false !== $i = strrpos($k, "\0")) {
+ $k = substr($k, 1 + $i);
+ }
+ $data[$k] = $v;
+ }
+
+ return $data;
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php b/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php
index b04f9b848ce67..76f4f7411229e 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php
@@ -16,6 +16,8 @@
*/
final class AbstractArgument
{
+ use ArgumentTrait;
+
private string $text;
private string $context = '';
diff --git a/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php b/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php
new file mode 100644
index 0000000000000..77b4b23331f7d
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Argument;
+
+/**
+ * Helps reduce the size of the dumped container when using php-serialize.
+ *
+ * @internal
+ */
+trait ArgumentTrait
+{
+ public function __serialize(): array
+ {
+ $data = [];
+ foreach ((array) $this as $k => $v) {
+ if (null === $v) {
+ continue;
+ }
+ if (false !== $i = strrpos($k, "\0")) {
+ $k = substr($k, 1 + $i);
+ }
+ $data[$k] = $v;
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php
index f704bc19a4776..b8b1df90cc8c6 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php
@@ -16,21 +16,29 @@
*/
final class BoundArgument implements ArgumentInterface
{
+ use ArgumentTrait;
+
public const SERVICE_BINDING = 0;
public const DEFAULTS_BINDING = 1;
public const INSTANCEOF_BINDING = 2;
private static int $sequence = 0;
+ private mixed $value = null;
private ?int $identifier = null;
private ?bool $used = null;
+ private int $type = 0;
+ private ?string $file = null;
public function __construct(
- private mixed $value,
+ mixed $value,
bool $trackUsage = true,
- private int $type = 0,
- private ?string $file = null,
+ int $type = 0,
+ ?string $file = null,
) {
+ $this->value = $value;
+ $this->type = $type;
+ $this->file = $file;
if ($trackUsage) {
$this->identifier = ++self::$sequence;
} else {
diff --git a/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php
index 1e2de6d98461b..fa44f22b929c4 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php
@@ -18,6 +18,8 @@
*/
class IteratorArgument implements ArgumentInterface
{
+ use ArgumentTrait;
+
private array $values;
public function __construct(array $values)
diff --git a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
index 7a677ebbd4e20..3e87186432efa 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
@@ -40,22 +40,22 @@ public function __get(mixed $name): mixed
}
if (isset($this->initializer)) {
- $this->service = ($this->initializer)();
+ if (\is_string($service = ($this->initializer)())) {
+ $service = (new \ReflectionClass($service))->newInstanceWithoutConstructor();
+ }
+ $this->service = $service;
unset($this->initializer);
}
return $this->service;
}
- public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string
+ public static function getCode(string $initializer, array $callable, string $class, ContainerBuilder $container, ?string $id): string
{
$method = $callable[1];
- $asClosure = 'Closure' === ($definition->getClass() ?: 'Closure');
- if ($asClosure) {
+ if ($asClosure = 'Closure' === $class) {
$class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass();
- } else {
- $class = $definition->getClass();
}
$r = $container->getReflectionClass($class);
diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php
index 3537540eda3e7..7fc2d3081aa69 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php
@@ -20,6 +20,8 @@
*/
class ServiceClosureArgument implements ArgumentInterface
{
+ use ArgumentTrait;
+
private array $values;
public function __construct(mixed $value)
diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php
index 555d14689a6bb..4983d83ac9518 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\DependencyInjection\Argument;
+use Symfony\Component\DependencyInjection\Loader\Configurator\Traits\ArgumentTrait;
+
/**
* Represents a closure acting as a service locator.
*
@@ -18,6 +20,8 @@
*/
class ServiceLocatorArgument implements ArgumentInterface
{
+ use ArgumentTrait;
+
private array $values;
private ?TaggedIteratorArgument $taggedIteratorArgument = null;
diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php
index 396cdf14475e2..2a9fdd72b73ee 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php
@@ -18,9 +18,9 @@
*/
class TaggedIteratorArgument extends IteratorArgument
{
- private mixed $indexAttribute;
- private ?string $defaultIndexMethod;
- private ?string $defaultPriorityMethod;
+ private mixed $indexAttribute = null;
+ private ?string $defaultIndexMethod = null;
+ private ?string $defaultPriorityMethod = null;
/**
* @param string $tag The name of the tag identifying the target services
diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php b/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php
index 0839afa48ff44..c74b0923dfedd 100644
--- a/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php
+++ b/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php
@@ -17,7 +17,7 @@
* @author Alan Poulain
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
-final class AsAlias
+class AsAlias
{
/**
* @var list
diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
index cc3306c739638..de751213acad5 100644
--- a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
+++ b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
@@ -20,8 +20,8 @@
class AsTaggedItem
{
/**
- * @param string|null $index The property or method to use to index the item in the locator
- * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the locator
+ * @param string|null $index The property or method to use to index the item in the iterator/locator
+ * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator
*/
public function __construct(
public ?string $index = null,
diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md
index df3486a9dc867..5c6c41cfdf27b 100644
--- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md
+++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Allow `#[AsAlias]` to be extended
+
7.3
---
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
index de4acb258c3a9..52af43f606256 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
@@ -69,6 +69,9 @@ public function process(ContainerBuilder $container): void
if (!$this->graph->hasNode($id)) {
continue;
}
+ if ($definition->isPublic()) {
+ $this->connectedIds[$id] = true;
+ }
foreach ($this->graph->getNode($id)->getOutEdges() as $edge) {
if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) {
$this->currentId = $id;
@@ -188,17 +191,13 @@ private function isInlineableDefinition(string $id, Definition $definition): boo
return true;
}
- if ($definition->isPublic()) {
+ if ($definition->isPublic()
+ || $this->currentId === $id
+ || !$this->graph->hasNode($id)
+ ) {
return false;
}
- if (!$this->graph->hasNode($id)) {
- return true;
- }
-
- if ($this->currentId === $id) {
- return false;
- }
$this->connectedIds[$id] = true;
$srcIds = [];
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
index 4befef860a66e..8c6b5b582770d 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
@@ -88,8 +88,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam
if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $reflector) {
$defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
}
- $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null;
- $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId;
+ $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId;
$services[] = [$priority, ++$i, $index, $serviceId, $class];
}
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php
index ea077cba9a5f0..87927ed248581 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php
@@ -20,25 +20,35 @@ class ResolveEnvPlaceholdersPass extends AbstractRecursivePass
{
protected bool $skipScalars = false;
+ /**
+ * @param string|true|null $format A sprintf() format returning the replacement for each env var name or
+ * null to resolve back to the original "%env(VAR)%" format or
+ * true to resolve to the actual values of the referenced env vars
+ */
+ public function __construct(
+ private string|bool|null $format = true,
+ ) {
+ }
+
protected function processValue(mixed $value, bool $isRoot = false): mixed
{
if (\is_string($value)) {
- return $this->container->resolveEnvPlaceholders($value, true);
+ return $this->container->resolveEnvPlaceholders($value, $this->format);
}
if ($value instanceof Definition) {
$changes = $value->getChanges();
if (isset($changes['class'])) {
- $value->setClass($this->container->resolveEnvPlaceholders($value->getClass(), true));
+ $value->setClass($this->container->resolveEnvPlaceholders($value->getClass(), $this->format));
}
if (isset($changes['file'])) {
- $value->setFile($this->container->resolveEnvPlaceholders($value->getFile(), true));
+ $value->setFile($this->container->resolveEnvPlaceholders($value->getFile(), $this->format));
}
}
$value = parent::processValue($value, $isRoot);
if ($value && \is_array($value) && !$isRoot) {
- $value = array_combine($this->container->resolveEnvPlaceholders(array_keys($value), true), $value);
+ $value = array_combine($this->container->resolveEnvPlaceholders(array_keys($value), $this->format), $value);
}
return $value;
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
index 81c14ac5cc4d0..eedc0f484243c 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
@@ -54,17 +54,41 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
$value->setClass(ServiceLocator::class);
}
- $services = $value->getArguments()[0] ?? null;
+ $values = $value->getArguments()[0] ?? null;
+ $services = [];
- if ($services instanceof TaggedIteratorArgument) {
- $services = $this->findAndSortTaggedServices($services, $this->container);
- }
-
- if (!\is_array($services)) {
+ if ($values instanceof TaggedIteratorArgument) {
+ foreach ($this->findAndSortTaggedServices($values, $this->container) as $k => $v) {
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ } elseif (!\is_array($values)) {
throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
+ } else {
+ $i = 0;
+
+ foreach ($values as $k => $v) {
+ if ($v instanceof ServiceClosureArgument) {
+ $services[$k] = $v;
+ continue;
+ }
+
+ if ($i === $k) {
+ if ($v instanceof Reference) {
+ $k = (string) $v;
+ }
+ ++$i;
+ } elseif (\is_int($k)) {
+ $i = null;
+ }
+
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ if (\count($services) === $i) {
+ ksort($services);
+ }
}
- $value->setArgument(0, self::map($services));
+ $value->setArgument(0, $services);
$id = '.service_locator.'.ContainerBuilder::hash($value);
@@ -83,8 +107,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference
{
+ foreach ($map as $k => $v) {
+ $map[$k] = new ServiceClosureArgument($v);
+ }
+
$locator = (new Definition(ServiceLocator::class))
- ->addArgument(self::map($map))
+ ->addArgument($map)
->addTag('container.service_locator');
if (null !== $callerId && $container->hasDefinition($callerId)) {
@@ -109,29 +137,4 @@ public static function register(ContainerBuilder $container, array $map, ?string
return new Reference($id);
}
-
- public static function map(array $services): array
- {
- $i = 0;
-
- foreach ($services as $k => $v) {
- if ($v instanceof ServiceClosureArgument) {
- continue;
- }
-
- if ($i === $k) {
- if ($v instanceof Reference) {
- unset($services[$k]);
- $k = (string) $v;
- }
- ++$i;
- } elseif (\is_int($k)) {
- $i = null;
- }
-
- $services[$k] = new ServiceClosureArgument($v);
- }
-
- return $services;
- }
}
diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
index 47202cf7d9e9a..38208124d3baf 100644
--- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
@@ -790,10 +790,11 @@ public function parameterCannotBeEmpty(string $name, string $message): void
* * The parameter bag is frozen;
* * Extension loading is disabled.
*
- * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved using the current
- * env vars or be replaced by uniquely identifiable placeholders.
- * Set to "true" when you want to use the current ContainerBuilder
- * directly, keep to "false" when the container is dumped instead.
+ * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved at build time using
+ * the current env var values (true), or be resolved at runtime based
+ * on the environment (false). In general, this should be set to "true"
+ * when you want to use the current ContainerBuilder directly, and to
+ * "false" when the container is dumped instead.
*/
public function compile(bool $resolveEnvPlaceholders = false): void
{
@@ -1108,14 +1109,15 @@ private function createService(Definition $definition, array &$inlineServices, b
}
if (\is_array($callable) && (
- $callable[0] instanceof Reference
+ 'Closure' !== $class
+ || $callable[0] instanceof Reference
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
)) {
$initializer = function () use ($callable, &$inlineServices) {
return $this->doResolveServices($callable[0], $inlineServices);
};
- $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';');
+ $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $class, $this, $id).';');
$this->shareService($definition, $proxy, $id, $inlineServices);
return $proxy;
diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php
index 61cc0b9d6785c..b410d02204636 100644
--- a/src/Symfony/Component/DependencyInjection/Definition.php
+++ b/src/Symfony/Component/DependencyInjection/Definition.php
@@ -820,4 +820,20 @@ public function hasErrors(): bool
{
return (bool) $this->errors;
}
+
+ public function __serialize(): array
+ {
+ $data = [];
+ foreach ((array) $this as $k => $v) {
+ if (false !== $i = strrpos($k, "\0")) {
+ $k = substr($k, 1 + $i);
+ }
+ if (!$v xor 'shared' === $k) {
+ continue;
+ }
+ $data[$k] = $v;
+ }
+
+ return $data;
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
index ee7e519a0c8aa..9568ad26b349c 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -1184,13 +1184,13 @@ private function addNewInstance(Definition $definition, string $return = '', ?st
throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
}
- if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && (
+ if (['...'] === $arguments && ('Closure' !== ($class = $definition->getClass() ?: 'Closure') || $definition->isLazy() && (
$callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
- )) {
+ ))) {
$initializer = 'fn () => '.$this->dumpValue($callable[0]);
- return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail;
+ return $return.LazyClosure::getCode($initializer, $callable, $class, $this->container, $id).$tail;
}
if ($callable[0] instanceof Reference
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
index ec115500bb0cf..d79e7b90408b2 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
@@ -50,18 +50,18 @@ public function dump(array $options = []): string
$this->dumper ??= new YmlDumper();
- return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices());
+ return $this->addParameters()."\n".$this->addServices();
}
private function addService(string $id, Definition $definition): string
{
- $code = " $id:\n";
+ $code = " {$this->dumper->dump($id)}:\n";
if ($class = $definition->getClass()) {
if (str_starts_with($class, '\\')) {
$class = substr($class, 1);
}
- $code .= \sprintf(" class: %s\n", $this->dumper->dump($class));
+ $code .= \sprintf(" class: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($class)));
}
if (!$definition->isPrivate()) {
@@ -87,7 +87,7 @@ private function addService(string $id, Definition $definition): string
}
if ($definition->getFile()) {
- $code .= \sprintf(" file: %s\n", $this->dumper->dump($definition->getFile()));
+ $code .= \sprintf(" file: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($definition->getFile())));
}
if ($definition->isSynthetic()) {
@@ -238,7 +238,7 @@ private function dumpCallable(mixed $callable): mixed
}
}
- return $callable;
+ return $this->container->resolveEnvPlaceholders($callable);
}
/**
@@ -299,7 +299,7 @@ private function dumpValue(mixed $value): mixed
if (\is_array($value)) {
$code = [];
foreach ($value as $k => $v) {
- $code[$k] = $this->dumpValue($v);
+ $code[$this->container->resolveEnvPlaceholders($k)] = $this->dumpValue($v);
}
return $code;
@@ -319,7 +319,7 @@ private function dumpValue(mixed $value): mixed
throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
}
- return $value;
+ return $this->container->resolveEnvPlaceholders($value);
}
private function getServiceCall(string $id, ?Reference $reference = null): string
@@ -359,7 +359,7 @@ private function prepareParameters(array $parameters, bool $escape = true): arra
$filtered[$key] = $value;
}
- return $escape ? $this->escape($filtered) : $filtered;
+ return $escape ? $this->container->resolveEnvPlaceholders($this->escape($filtered)) : $filtered;
}
private function escape(array $arguments): array
diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
index bc38767bcb546..3cf23cf98eab4 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
@@ -216,7 +216,7 @@ public function registerClasses(Definition $prototype, string $namespace, string
}
$r = $this->container->getReflectionClass($class);
$defaultAlias = 1 === \count($interfaces) ? $interfaces[0] : null;
- foreach ($r->getAttributes(AsAlias::class) as $attr) {
+ foreach ($r->getAttributes(AsAlias::class, \ReflectionAttribute::IS_INSTANCEOF) as $attr) {
/** @var AsAlias $attribute */
$attribute = $attr->newInstance();
$alias = $attribute->id ?? $defaultAlias;
diff --git a/src/Symfony/Component/DependencyInjection/Reference.php b/src/Symfony/Component/DependencyInjection/Reference.php
index df7d173c53723..9a9d83fb1f457 100644
--- a/src/Symfony/Component/DependencyInjection/Reference.php
+++ b/src/Symfony/Component/DependencyInjection/Reference.php
@@ -34,6 +34,22 @@ public function __toString(): string
*/
public function getInvalidBehavior(): int
{
- return $this->invalidBehavior;
+ return $this->invalidBehavior ??= ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
+ }
+
+ public function __serialize(): array
+ {
+ $data = [];
+ foreach ((array) $this as $k => $v) {
+ if (false !== $i = strrpos($k, "\0")) {
+ $k = substr($k, 1 + $i);
+ }
+ if ('invalidBehavior' === $k && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $v) {
+ continue;
+ }
+ $data[$k] = $v;
+ }
+
+ return $data;
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
index 428227d19e2bc..e1b5c2a90f2ed 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
@@ -33,7 +33,7 @@ public function testThrowsWhenNotUsingInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because "Symfony\Component\DependencyInjection\Tests\Argument\LazyClosureTest" is not an interface.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(self::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], self::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnNonFunctionalInterface()
@@ -41,7 +41,7 @@ public function testThrowsOnNonFunctionalInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because interface "Symfony\Component\DependencyInjection\Tests\Argument\NonFunctionalInterface" doesn\'t have exactly one method.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(NonFunctionalInterface::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], NonFunctionalInterface::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnUnknownMethodInInterface()
@@ -49,7 +49,7 @@ public function testThrowsOnUnknownMethodInInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create lazy closure for service "bar" because its corresponding callable is invalid.');
- LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], new Definition(\Closure::class), new ContainerBuilder(), 'bar');
+ LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], \Closure::class, new ContainerBuilder(), 'bar');
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php
index e7b36d3ce496a..ffbdc180f5dbc 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php
@@ -452,7 +452,7 @@ public static function getSubscribedServices(): array
'autowired' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])),
'autowired.nullable' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])),
'autowired.parameter' => new ServiceClosureArgument('foobar'),
- 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.oNVewcO.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
+ 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.Di.wrC8.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])),
];
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
index 812b47c7a6f1f..ad9c62d9b387f 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
@@ -83,7 +83,27 @@ public function testProcessValue()
$this->assertSame(CustomDefinition::class, $locator('bar')::class);
$this->assertSame(CustomDefinition::class, $locator('baz')::class);
$this->assertSame(CustomDefinition::class, $locator('some.service')::class);
- $this->assertSame(CustomDefinition::class, \get_class($locator('inlines.service')));
+ $this->assertSame(CustomDefinition::class, $locator('inlines.service')::class);
+ }
+
+ public function testServiceListIsOrdered()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', CustomDefinition::class);
+ $container->register('baz', CustomDefinition::class);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([[
+ new Reference('baz'),
+ new Reference('bar'),
+ ]])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ $this->assertSame(['bar', 'baz'], array_keys($container->getDefinition('foo')->getArgument(0)));
}
public function testServiceWithKeyOverwritesPreviousInheritedKey()
@@ -170,6 +190,27 @@ public function testTaggedServices()
$this->assertSame(TestDefinition2::class, $locator('baz')::class);
}
+ public function testTaggedServicesKeysAreKept()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', TestDefinition1::class)->addTag('test_tag', ['index' => 0]);
+ $container->register('baz', TestDefinition2::class)->addTag('test_tag', ['index' => 1]);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([new TaggedIteratorArgument('test_tag', 'index', null, true)])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ /** @var ServiceLocator $locator */
+ $locator = $container->get('foo');
+
+ $this->assertSame(TestDefinition1::class, $locator(0)::class);
+ $this->assertSame(TestDefinition2::class, $locator(1)::class);
+ }
+
public function testIndexedByServiceIdWithDecoration()
{
$container = new ContainerBuilder();
@@ -201,15 +242,33 @@ public function testIndexedByServiceIdWithDecoration()
static::assertInstanceOf(DecoratedService::class, $locator->get(Service::class));
}
- public function testDefinitionOrderIsTheSame()
+ public function testServicesKeysAreKept()
{
$container = new ContainerBuilder();
$container->register('service-1');
$container->register('service-2');
+ $container->register('service-3');
$locator = ServiceLocatorTagPass::register($container, [
- new Reference('service-2'),
new Reference('service-1'),
+ 'service-2' => new Reference('service-2'),
+ 'foo' => new Reference('service-3'),
+ ]);
+ $locator = $container->getDefinition($locator);
+ $factories = $locator->getArguments()[0];
+
+ static::assertSame([0, 'service-2', 'foo'], array_keys($factories));
+ }
+
+ public function testDefinitionOrderIsTheSame()
+ {
+ $container = new ContainerBuilder();
+ $container->register('service-1');
+ $container->register('service-2');
+
+ $locator = ServiceLocatorTagPass::register($container, [
+ 'service-2' => new Reference('service-2'),
+ 'service-1' => new Reference('service-1'),
]);
$locator = $container->getDefinition($locator);
$factories = $locator->getArguments()[0];
diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
index 774b1f88b66e7..5e08e47ab908c 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
@@ -50,6 +50,7 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
+use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable;
use Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
@@ -532,6 +533,19 @@ public function testClosureProxy()
$this->assertInstanceOf(Foo::class, $container->get('closure_proxy')->theMethod());
}
+ public function testClosureProxyWithStaticMethod()
+ {
+ $container = new ContainerBuilder();
+ $container->register('closure_proxy', SingleMethodInterface::class)
+ ->setPublic('true')
+ ->setFactory(['Closure', 'fromCallable'])
+ ->setArguments([[MyCallable::class, 'theMethodImpl']]);
+ $container->compile();
+
+ $this->assertInstanceOf(SingleMethodInterface::class, $container->get('closure_proxy'));
+ $this->assertSame(124, $container->get('closure_proxy')->theMethod());
+ }
+
public function testCreateServiceClass()
{
$builder = new ContainerBuilder();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
index f9ff3fff786a3..3a21d7aa9a9c5 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
@@ -215,6 +215,26 @@ public function testDumpNonScalarTags()
$this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump());
}
+ public function testDumpResolvedEnvPlaceholders()
+ {
+ $container = new ContainerBuilder();
+ $container->setParameter('%env(PARAMETER_NAME)%', '%env(PARAMETER_VALUE)%');
+ $container
+ ->register('service', '%env(SERVICE_CLASS)%')
+ ->setFile('%env(SERVICE_FILE)%')
+ ->addArgument('%env(SERVICE_ARGUMENT)%')
+ ->setProperty('%env(SERVICE_PROPERTY_NAME)%', '%env(SERVICE_PROPERTY_VALUE)%')
+ ->addMethodCall('%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%'])
+ ->setFactory('%env(SERVICE_FACTORY)%')
+ ->setConfigurator('%env(SERVICE_CONFIGURATOR)%')
+ ->setPublic(true)
+ ;
+ $container->compile();
+ $dumper = new YamlDumper($container);
+
+ $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/container_with_env_placeholders.yml'), $dumper->dump());
+ }
+
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
{
$parser = new Parser();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithCustomAsAlias.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithCustomAsAlias.php
new file mode 100644
index 0000000000000..4f141909890d2
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithCustomAsAlias.php
@@ -0,0 +1,25 @@
+ 'foo'], constructor: 'create')]
class StaticConstructorAutoconfigure
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
index d72d7b3aec63a..9e07d0283e396 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
@@ -462,6 +462,11 @@ class MyCallable
public function __invoke(): void
{
}
+
+ public static function theMethodImpl(): int
+ {
+ return 124;
+ }
}
class MyInlineService
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
index 216dca434e489..ccd8d2e0bf63b 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
@@ -50,6 +50,6 @@ public function getRemovedIds(): array
*/
protected static function getBarService($container)
{
- return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } });
+ return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } });
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
new file mode 100644
index 0000000000000..46c91130faecd
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
@@ -0,0 +1,19 @@
+parameters:
+ '%env(PARAMETER_NAME)%': '%env(PARAMETER_VALUE)%'
+
+services:
+ service_container:
+ class: Symfony\Component\DependencyInjection\ContainerInterface
+ public: true
+ synthetic: true
+ service:
+ class: '%env(SERVICE_CLASS)%'
+ public: true
+ file: '%env(SERVICE_FILE)%'
+ arguments: ['%env(SERVICE_ARGUMENT)%']
+ properties: { '%env(SERVICE_PROPERTY_NAME)%': '%env(SERVICE_PROPERTY_VALUE)%' }
+ calls:
+ - ['%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%']]
+
+ factory: '%env(SERVICE_FACTORY)%'
+ configurator: '%env(SERVICE_CONFIGURATOR)%'
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
index 0ad1b363cf6bf..17bc228c77b82 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
@@ -45,6 +45,7 @@
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasMultiple;
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasProdEnv;
+use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithCustomAsAlias;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Utils\NotAService;
class FileLoaderTest extends TestCase
@@ -368,6 +369,8 @@ public function testRegisterClassesWithAsAlias(string $resource, array $expected
public static function provideResourcesWithAsAliasAttributes(): iterable
{
yield 'Private' => ['PrototypeAsAlias/{WithAsAlias,AliasFooInterface}.php', [AliasFooInterface::class => new Alias(WithAsAlias::class)]];
+ yield 'PrivateCustomAlias' => ['PrototypeAsAlias/{WithCustomAsAlias,AliasFooInterface}.php', [AliasFooInterface::class => new Alias(WithCustomAsAlias::class)], 'prod'];
+ yield 'PrivateCustomAliasNoMatch' => ['PrototypeAsAlias/{WithCustomAsAlias,AliasFooInterface}.php', [], 'dev'];
yield 'Interface' => ['PrototypeAsAlias/{WithAsAliasInterface,AliasFooInterface}.php', [AliasFooInterface::class => new Alias(WithAsAliasInterface::class)]];
yield 'Multiple' => ['PrototypeAsAlias/{WithAsAliasMultiple,AliasFooInterface}.php', [
AliasFooInterface::class => new Alias(WithAsAliasMultiple::class, true),
diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json
index 460751088f451..7b1e731b7d2eb 100644
--- a/src/Symfony/Component/DependencyInjection/composer.json
+++ b/src/Symfony/Component/DependencyInjection/composer.json
@@ -20,12 +20,12 @@
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^3.5",
- "symfony/var-exporter": "^6.4.20|^7.2.5"
+ "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0"
},
"require-dev": {
- "symfony/yaml": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0"
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0"
},
"conflict": {
"ext-psr": "<1.1|>=2",
diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php
index e41875e268203..7550da83d4fdb 100644
--- a/src/Symfony/Component/DomCrawler/Crawler.php
+++ b/src/Symfony/Component/DomCrawler/Crawler.php
@@ -747,12 +747,12 @@ public function selectImage(string $value): static
}
/**
- * Selects a button by name or alt value for images.
+ * Selects a button by its text content, id, value, name or alt attribute.
*/
public function selectButton(string $value): static
{
return $this->filterRelativeXPath(
- \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
+ \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
);
}
diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
index 5cdbbbf45870d..53169efcab8e5 100644
--- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
+++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
@@ -452,10 +452,10 @@ public function testFilterXpathComplexQueries()
$this->assertCount(0, $crawler->filterXPath('/body'));
$this->assertCount(1, $crawler->filterXPath('./body'));
$this->assertCount(1, $crawler->filterXPath('.//body'));
- $this->assertCount(5, $crawler->filterXPath('.//input'));
+ $this->assertCount(6, $crawler->filterXPath('.//input'));
$this->assertCount(4, $crawler->filterXPath('//form')->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('body'));
- $this->assertCount(6, $crawler->filterXPath('//button | //input'));
+ $this->assertCount(8, $crawler->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('//body'));
$this->assertCount(1, $crawler->filterXPath('descendant-or-self::body'));
$this->assertCount(1, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('./div'), 'A child selection finds only the current div');
@@ -723,16 +723,23 @@ public function testSelectButton()
$this->assertNotSame($crawler, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
$this->assertInstanceOf(Crawler::class, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
- $this->assertEquals(1, $crawler->selectButton('FooValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('FooValue'), '->selectButton() selects type-submit inputs by value');
+ $this->assertCount(1, $crawler->selectButton('FooName'), '->selectButton() selects type-submit inputs by name');
+ $this->assertCount(1, $crawler->selectButton('FooId'), '->selectButton() selects type-submit inputs by id');
- $this->assertEquals(1, $crawler->selectButton('BarValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('BarValue'), '->selectButton() selects type-button inputs by value');
+ $this->assertCount(1, $crawler->selectButton('BarName'), '->selectButton() selects type-button inputs by name');
+ $this->assertCount(1, $crawler->selectButton('BarId'), '->selectButton() selects type-button inputs by id');
- $this->assertEquals(1, $crawler->selectButton('FooBarValue')->count(), '->selectButton() selects buttons with form attribute too');
- $this->assertEquals(1, $crawler->selectButton('FooBarName')->count(), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('ImageAlt'), '->selectButton() selects type-image inputs by alt');
+
+ $this->assertCount(1, $crawler->selectButton('ButtonValue'), '->selectButton() selects buttons by value');
+ $this->assertCount(1, $crawler->selectButton('ButtonName'), '->selectButton() selects buttons by name');
+ $this->assertCount(1, $crawler->selectButton('ButtonId'), '->selectButton() selects buttons by id');
+ $this->assertCount(1, $crawler->selectButton('ButtonText'), '->selectButton() selects buttons by text content');
+
+ $this->assertCount(1, $crawler->selectButton('FooBarValue'), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('FooBarName'), '->selectButton() selects buttons with form attribute too');
}
public function testSelectButtonWithSingleQuotesInNameAttribute()
@@ -1322,6 +1329,9 @@ public function createTestCrawler($uri = null)
+
+
+
- One
- Two
diff --git a/src/Symfony/Component/DomCrawler/composer.json b/src/Symfony/Component/DomCrawler/composer.json
index c47482794d0a0..0e5c984d09be2 100644
--- a/src/Symfony/Component/DomCrawler/composer.json
+++ b/src/Symfony/Component/DomCrawler/composer.json
@@ -22,7 +22,7 @@
"masterminds/html5": "^2.6"
},
"require-dev": {
- "symfony/css-selector": "^6.4|^7.0"
+ "symfony/css-selector": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\DomCrawler\\": "" },
diff --git a/src/Symfony/Component/Dotenv/README.md b/src/Symfony/Component/Dotenv/README.md
index 2a1cc02ccfcb8..67ff66a07a802 100644
--- a/src/Symfony/Component/Dotenv/README.md
+++ b/src/Symfony/Component/Dotenv/README.md
@@ -11,6 +11,15 @@ Getting Started
composer require symfony/dotenv
```
+Usage
+-----
+
+> For an .env file with this format:
+
+```env
+YOUR_VARIABLE_NAME=my-string
+```
+
```php
use Symfony\Component\Dotenv\Dotenv;
@@ -25,6 +34,12 @@ $dotenv->overload(__DIR__.'/.env');
// loads .env, .env.local, and .env.$APP_ENV.local or .env.$APP_ENV
$dotenv->loadEnv(__DIR__.'/.env');
+
+// Usage with $_ENV
+$envVariable = $_ENV['YOUR_VARIABLE_NAME'];
+
+// Usage with $_SERVER
+$envVariable = $_SERVER['YOUR_VARIABLE_NAME'];
```
Resources
diff --git a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
index 28c0b48ca46fa..57828291ae86d 100644
--- a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
+++ b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
@@ -288,7 +288,11 @@ public function testCompletion()
$command = new DebugCommand($env, $projectDirectory);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('debug:dotenv'));
$this->assertSame(['FOO', 'TEST'], $tester->complete(['']));
}
diff --git a/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php b/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php
index 44fc304c5ef8f..d2f2dfecb4dc7 100644
--- a/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php
+++ b/src/Symfony/Component/Dotenv/Tests/Command/DotenvDumpCommandTest.php
@@ -95,7 +95,12 @@ public function testExecuteTestEnvs()
private function createCommand(): CommandTester
{
$application = new Application();
- $application->add(new DotenvDumpCommand(__DIR__));
+ $command = new DotenvDumpCommand(__DIR__);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return new CommandTester($application->find('dotenv:dump'));
}
diff --git a/src/Symfony/Component/Dotenv/composer.json b/src/Symfony/Component/Dotenv/composer.json
index 34c4718a76aeb..fe887ff0a31c5 100644
--- a/src/Symfony/Component/Dotenv/composer.json
+++ b/src/Symfony/Component/Dotenv/composer.json
@@ -19,8 +19,8 @@
"php": ">=8.2"
},
"require-dev": {
- "symfony/console": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0"
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/console": "<6.4",
diff --git a/src/Symfony/Component/Emoji/Resources/bin/composer.json b/src/Symfony/Component/Emoji/Resources/bin/composer.json
index 29bf4d6466941..a120970a9bfb9 100644
--- a/src/Symfony/Component/Emoji/Resources/bin/composer.json
+++ b/src/Symfony/Component/Emoji/Resources/bin/composer.json
@@ -15,9 +15,9 @@
],
"minimum-stability": "dev",
"require": {
- "symfony/filesystem": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0",
"unicode-org/cldr": "*"
}
}
diff --git a/src/Symfony/Component/Emoji/composer.json b/src/Symfony/Component/Emoji/composer.json
index 9d9414c224aac..d4a6a083a108b 100644
--- a/src/Symfony/Component/Emoji/composer.json
+++ b/src/Symfony/Component/Emoji/composer.json
@@ -20,9 +20,9 @@
"ext-intl": "*"
},
"require-dev": {
- "symfony/filesystem": "^7.1",
- "symfony/finder": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/filesystem": "^7.1|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Emoji\\": "" },
diff --git a/src/Symfony/Component/ErrorHandler/Debug.php b/src/Symfony/Component/ErrorHandler/Debug.php
index d54a38c4cac12..b090040d024b4 100644
--- a/src/Symfony/Component/ErrorHandler/Debug.php
+++ b/src/Symfony/Component/ErrorHandler/Debug.php
@@ -20,7 +20,7 @@ class Debug
{
public static function enable(): ErrorHandler
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', 0);
diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
index d0d1246a1ddbd..7bd9a083e53a4 100644
--- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
+++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
@@ -233,6 +233,10 @@ private function formatFile(string $file, int $line, ?string $text = null): stri
$text .= ' at line '.$line;
}
+ if (!file_exists($file)) {
+ return $text;
+ }
+
$link = $this->fileLinkFormat->format($file, $line);
return \sprintf('%s', $this->escape($link), $text);
diff --git a/src/Symfony/Component/ErrorHandler/Tests/Command/ErrorDumpCommandTest.php b/src/Symfony/Component/ErrorHandler/Tests/Command/ErrorDumpCommandTest.php
index 670adbdb12907..0a0ae20b9c91c 100644
--- a/src/Symfony/Component/ErrorHandler/Tests/Command/ErrorDumpCommandTest.php
+++ b/src/Symfony/Component/ErrorHandler/Tests/Command/ErrorDumpCommandTest.php
@@ -102,11 +102,16 @@ private function getCommandTester(KernelInterface $kernel): CommandTester
$entrypointLookup = $this->createMock(EntrypointLookupInterface::class);
$application = new Application($kernel);
- $application->add(new ErrorDumpCommand(
+ $command = new ErrorDumpCommand(
new Filesystem(),
$errorRenderer,
$entrypointLookup,
- ));
+ );
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return new CommandTester($application->find('error:dump'));
}
diff --git a/src/Symfony/Component/ErrorHandler/composer.json b/src/Symfony/Component/ErrorHandler/composer.json
index 98b94328364e8..dc56a36e414d0 100644
--- a/src/Symfony/Component/ErrorHandler/composer.json
+++ b/src/Symfony/Component/ErrorHandler/composer.json
@@ -18,12 +18,12 @@
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/console": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/webpack-encore-bundle": "^1.0|^2.0"
},
diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json
index 598bbdc5489a4..d125e7608f25f 100644
--- a/src/Symfony/Component/EventDispatcher/composer.json
+++ b/src/Symfony/Component/EventDispatcher/composer.json
@@ -20,13 +20,13 @@
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json
index e24a315dae873..e3989cdefbf08 100644
--- a/src/Symfony/Component/ExpressionLanguage/composer.json
+++ b/src/Symfony/Component/ExpressionLanguage/composer.json
@@ -17,7 +17,7 @@
],
"require": {
"php": ">=8.2",
- "symfony/cache": "^6.4|^7.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^2.5|^3"
},
diff --git a/src/Symfony/Component/Filesystem/composer.json b/src/Symfony/Component/Filesystem/composer.json
index c781e55b18438..42bbfa08a7a00 100644
--- a/src/Symfony/Component/Filesystem/composer.json
+++ b/src/Symfony/Component/Filesystem/composer.json
@@ -21,7 +21,7 @@
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^6.4|^7.0"
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Filesystem\\": "" },
diff --git a/src/Symfony/Component/Finder/composer.json b/src/Symfony/Component/Finder/composer.json
index 2b70600d097cd..52bdb48162417 100644
--- a/src/Symfony/Component/Finder/composer.json
+++ b/src/Symfony/Component/Finder/composer.json
@@ -19,7 +19,7 @@
"php": ">=8.2"
},
"require-dev": {
- "symfony/filesystem": "^6.4|^7.0"
+ "symfony/filesystem": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Finder\\": "" },
diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md
index 00d3b2fc4027b..b74d43e79d23f 100644
--- a/src/Symfony/Component/Form/CHANGELOG.md
+++ b/src/Symfony/Component/Form/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Add `input=date_point` to `DateTimeType`, `DateType` and `TimeType`
+
7.3
---
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DatePointToDateTimeTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DatePointToDateTimeTransformer.php
new file mode 100644
index 0000000000000..dc1f7506822f9
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DatePointToDateTimeTransformer.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Clock\DatePoint;
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between a DatePoint object and a DateTime object.
+ *
+ * @implements DataTransformerInterface
+ */
+final class DatePointToDateTimeTransformer implements DataTransformerInterface
+{
+ /**
+ * Transforms a DatePoint into a DateTime object.
+ *
+ * @param DatePoint|null $value A DatePoint object
+ *
+ * @throws TransformationFailedException If the given value is not a DatePoint
+ */
+ public function transform(mixed $value): ?\DateTime
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!$value instanceof DatePoint) {
+ throw new TransformationFailedException(\sprintf('Expected a "%s".', DatePoint::class));
+ }
+
+ return \DateTime::createFromImmutable($value);
+ }
+
+ /**
+ * Transforms a DateTime object into a DatePoint object.
+ *
+ * @param \DateTime|null $value A DateTime object
+ *
+ * @throws TransformationFailedException If the given value is not a \DateTime
+ */
+ public function reverseTransform(mixed $value): ?DatePoint
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!$value instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ return DatePoint::createFromMutable($value);
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
index e9c9cb9e55259..a0b3434a6b828 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
@@ -39,14 +39,14 @@ public function __construct(
/**
* Transforms a normalized format into a localized money string.
*
- * @param int|float|null $value Normalized number
+ * @param int|float|string|null $value Normalized number
*
* @throws TransformationFailedException if the given value is not numeric or
* if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null !== $value && 1 !== $this->divisor) {
+ if (null !== $value && '' !== $value && 1 !== $this->divisor) {
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
index 3020dd1483c28..ffcbc1feee6d7 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
@@ -41,14 +41,14 @@ public function __construct(
/**
* Transforms a number type into localized number.
*
- * @param int|float|null $value Number value
+ * @param int|float|string|null $value Number value
*
* @throws TransformationFailedException if the given value is not numeric
* or if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null === $value) {
+ if (null === $value || '' === $value) {
return '';
}
diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
index 299f919373403..a7da65bdb60fa 100644
--- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
+++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
@@ -199,7 +199,13 @@ public function onSubmit(FormEvent $event): void
}
if ($this->keepAsList) {
- $formReindex = [];
+ $formReindex = $dataKeys = [];
+ foreach ($data as $key => $value) {
+ $dataKeys[] = $key;
+ }
+ foreach ($dataKeys as $key) {
+ unset($data[$key]);
+ }
foreach ($form as $name => $child) {
$formReindex[] = $child;
$form->remove($name);
@@ -207,9 +213,9 @@ public function onSubmit(FormEvent $event): void
foreach ($formReindex as $index => $child) {
$form->add($index, $this->type, array_replace([
'property_path' => '['.$index.']',
- ], $this->options));
+ ], $this->options, ['data' => $child->getData()]));
+ $data[$index] = $child->getData();
}
- $data = array_values($data);
}
$event->setData($data);
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
index cf4c2b7416be9..8ecaa63c078b8 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
@@ -11,10 +11,12 @@
namespace Symfony\Component\Form\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DatePointToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
@@ -178,7 +180,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
;
}
- if ('datetime_immutable' === $options['input']) {
+ if ('date_point' === $options['input']) {
+ if (!class_exists(DatePoint::class)) {
+ throw new LogicException(\sprintf('The "symfony/clock" component is required to use "%s" with option "input=date_point". Try running "composer require symfony/clock".', self::class));
+ }
+ $builder->addModelTransformer(new DatePointToDateTimeTransformer());
+ } elseif ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
@@ -194,7 +201,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
));
}
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
+ if (\in_array($options['input'], ['datetime', 'datetime_immutable', 'date_point'], true) && null !== $options['model_timezone']) {
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
$date = $event->getData();
@@ -283,6 +290,7 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
+ 'date_point',
'string',
'timestamp',
'array',
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
index 36b430e144b58..5c8dfaa3c2b10 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php
@@ -11,8 +11,10 @@
namespace Symfony\Component\Form\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DatePointToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
@@ -156,7 +158,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
;
}
- if ('datetime_immutable' === $options['input']) {
+ if ('date_point' === $options['input']) {
+ if (!class_exists(DatePoint::class)) {
+ throw new LogicException(\sprintf('The "symfony/clock" component is required to use "%s" with option "input=date_point". Try running "composer require symfony/clock".', self::class));
+ }
+ $builder->addModelTransformer(new DatePointToDateTimeTransformer());
+ } elseif ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
@@ -172,7 +179,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
));
}
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
+ if (\in_array($options['input'], ['datetime', 'datetime_immutable', 'date_point'], true) && null !== $options['model_timezone']) {
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
$date = $event->getData();
@@ -298,6 +305,7 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
+ 'date_point',
'string',
'timestamp',
'array',
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
index 92cf42d963e74..1622301aed631 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
@@ -11,9 +11,11 @@
namespace Symfony\Component\Form\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DatePointToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
@@ -190,7 +192,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
}
- if ('datetime_immutable' === $options['input']) {
+ if ('date_point' === $options['input']) {
+ if (!class_exists(DatePoint::class)) {
+ throw new LogicException(\sprintf('The "symfony/clock" component is required to use "%s" with option "input=date_point". Try running "composer require symfony/clock".', self::class));
+ }
+ $builder->addModelTransformer(new DatePointToDateTimeTransformer());
+ } elseif ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
@@ -206,7 +213,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
));
}
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
+ if (\in_array($options['input'], ['datetime', 'datetime_immutable', 'date_point'], true) && null !== $options['model_timezone']) {
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
$date = $event->getData();
@@ -354,6 +361,7 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
+ 'date_point',
'string',
'timestamp',
'array',
diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php
index cac92addbf790..c20c72d8d2aa2 100644
--- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php
+++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php
@@ -194,7 +194,11 @@ public function testComplete(array $input, array $expectedSuggestions)
$formRegistry = new FormRegistry([], new ResolvedFormTypeFactory());
$command = new DebugCommand($formRegistry);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('debug:form'));
$this->assertSame($expectedSuggestions, $tester->complete($input));
}
@@ -278,7 +282,11 @@ private function createCommandTester(array $namespaces = ['Symfony\Component\For
$formRegistry = new FormRegistry([], new ResolvedFormTypeFactory());
$command = new DebugCommand($formRegistry, $namespaces, $types);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return new CommandTester($application->find('debug:form'));
}
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
index e5733ad96abb5..689c6f0d4da32 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
@@ -54,6 +54,13 @@ public function testTransformExpectsNumeric()
$transformer->transform('abcd');
}
+ public function testTransformEmptyString()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->assertSame('', $transformer->transform(''));
+ }
+
public function testTransformEmpty()
{
$transformer = new MoneyToLocalizedStringTransformer();
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
index 37448db51030a..c0344b9f232ea 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
@@ -49,6 +49,7 @@ public static function provideTransformations()
{
return [
[null, '', 'de_AT'],
+ ['', '', 'de_AT'],
[1, '1', 'de_AT'],
[1.5, '1,5', 'de_AT'],
[1234.5, '1234,5', 'de_AT'],
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
index 934460c8f98a4..390f6b04a60c5 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
@@ -310,7 +310,7 @@ public function testOnSubmitDealsWithObjectBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
- public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
+ public function testOnSubmitDealsWithDoctrineCollection()
{
$this->builder->add($this->getBuilder('1'));
@@ -323,6 +323,19 @@ public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
+ public function testKeepAsListWorksWithTraversableArrayAccess()
+ {
+ $this->builder->add($this->getBuilder('1'));
+
+ $data = new \ArrayIterator([0 => 'first', 1 => 'second', 2 => 'third']);
+ $event = new FormEvent($this->builder->getForm(), $data);
+ $listener = new ResizeFormListener(TextType::class, keepAsList: true);
+ $listener->onSubmit($event);
+
+ $this->assertCount(1, $event->getData());
+ $this->assertArrayHasKey(0, $event->getData());
+ }
+
public function testOnSubmitDeleteEmptyNotCompoundEntriesIfAllowDelete()
{
$this->builder->setData(['0' => 'first', '1' => 'second']);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php
index 5067bb05e7258..e655af51f7cef 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormError;
@@ -62,6 +63,37 @@ public function testSubmitDateTime()
$this->assertEquals($dateTime, $form->getData());
}
+ public function testSubmitDatePoint()
+ {
+ $form = $this->factory->create(static::TESTED_TYPE, null, [
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'date_widget' => 'choice',
+ 'years' => [2010],
+ 'time_widget' => 'choice',
+ 'input' => 'date_point',
+ ]);
+
+ $input = [
+ 'date' => [
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ],
+ 'time' => [
+ 'hour' => '3',
+ 'minute' => '4',
+ ],
+ ];
+
+ $form->submit($input);
+
+ $this->assertInstanceOf(DatePoint::class, $form->getData());
+ $datePoint = DatePoint::createFromMutable(new \DateTime('2010-06-02 03:04:00 UTC'));
+ $this->assertEquals($datePoint, $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
public function testSubmitDateTimeImmutable()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php
index 5f4f896b5daed..b2af6f4bf8b13 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\Type\DateType;
@@ -115,6 +116,27 @@ public function testSubmitFromSingleTextDateTime()
$this->assertEquals('02.06.2010', $form->getViewData());
}
+ public function testSubmitFromSingleTextDatePoint()
+ {
+ if (!class_exists(DatePoint::class)) {
+ self::markTestSkipped('The DatePoint class is not available.');
+ }
+
+ $form = $this->factory->create(static::TESTED_TYPE, null, [
+ 'html5' => false,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'date_point',
+ ]);
+
+ $form->submit('2010-06-02');
+
+ $this->assertInstanceOf(DatePoint::class, $form->getData());
+ $this->assertEquals(DatePoint::createFromMutable(new \DateTime('2010-06-02 UTC')), $form->getData());
+ $this->assertEquals('2010-06-02', $form->getViewData());
+ }
+
public function testSubmitFromSingleTextDateTimeImmutable()
{
// we test against "de_DE", so we need the full implementation
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php
index 8a2baf1b4c708..6711fa55b6322 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+use Symfony\Component\Clock\DatePoint;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Exception\LogicException;
@@ -45,6 +46,32 @@ public function testSubmitDateTime()
$this->assertEquals($input, $form->getViewData());
}
+ public function testSubmitDatePoint()
+ {
+ if (!class_exists(DatePoint::class)) {
+ self::markTestSkipped('The DatePoint class is not available.');
+ }
+
+ $form = $this->factory->create(static::TESTED_TYPE, null, [
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ 'input' => 'date_point',
+ ]);
+
+ $input = [
+ 'hour' => '3',
+ 'minute' => '4',
+ ];
+
+ $form->submit($input);
+
+ $this->assertInstanceOf(DatePoint::class, $form->getData());
+ $datePoint = DatePoint::createFromMutable(new \DateTime('1970-01-01 03:04:00 UTC'));
+ $this->assertEquals($datePoint, $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
public function testSubmitDateTimeImmutable()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
index 2dec87b5c712c..bd52831e28c3d 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
@@ -257,7 +257,7 @@ public function testCollectionTypeKeepAsListOptionTrue()
{
$formMetadata = new ClassMetadata(Form::class);
$authorMetadata = (new ClassMetadata(Author::class))
- ->addPropertyConstraint('firstName', new NotBlank());
+ ->addPropertyConstraint('firstName', new Length(1));
$organizationMetadata = (new ClassMetadata(Organization::class))
->addPropertyConstraint('authors', new Valid());
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
@@ -301,22 +301,22 @@ public function testCollectionTypeKeepAsListOptionTrue()
$form->submit([
'authors' => [
0 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'foobar', // Fires a Length Error
'lastName' => 'lastName1',
],
// key "1" could be missing if we add 4 blank form entries and then remove it.
2 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barfoo', // Fires a Length Error
'lastName' => 'lastName3',
],
3 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barbaz', // Fires a Length Error
'lastName' => 'lastName3',
],
],
]);
- // Form does have 3 not blank errors
+ // Form does have 3 length errors
$errors = $form->getErrors(true);
$this->assertCount(3, $errors);
@@ -328,12 +328,15 @@ public function testCollectionTypeKeepAsListOptionTrue()
];
$this->assertTrue($form->get('authors')->has('0'));
+ $this->assertSame('foobar', $form->get('authors')->get('0')->getData()->firstName);
$this->assertContains('data.authors[0].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('1'));
+ $this->assertSame('barfoo', $form->get('authors')->get('1')->getData()->firstName);
$this->assertContains('data.authors[1].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('2'));
+ $this->assertSame('barbaz', $form->get('authors')->get('2')->getData()->firstName);
$this->assertContains('data.authors[2].firstName', $errorPaths);
$this->assertFalse($form->get('authors')->has('3'));
diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json
index f4403ba74d878..8ed9a50124d6f 100644
--- a/src/Symfony/Component/Form/composer.json
+++ b/src/Symfony/Component/Form/composer.json
@@ -18,30 +18,31 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/options-resolver": "^7.3",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/options-resolver": "^7.3|^8.0",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/polyfill-mbstring": "~1.0",
- "symfony/property-access": "^6.4|^7.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
"doctrine/collections": "^1.0|^2.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/html-sanitizer": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
- "symfony/security-core": "^6.4|^7.0",
- "symfony/security-csrf": "^6.4|^7.0",
- "symfony/translation": "^6.4.3|^7.0.3",
- "symfony/var-dumper": "^6.4|^7.0",
- "symfony/uid": "^6.4|^7.0"
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/html-sanitizer": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/security-csrf": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4.3|^7.0.3|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/console": "<6.4",
diff --git a/src/Symfony/Component/HttpClient/AmpHttpClient.php b/src/Symfony/Component/HttpClient/AmpHttpClient.php
index 4c73fbaf3db24..b45229fa8d6b9 100644
--- a/src/Symfony/Component/HttpClient/AmpHttpClient.php
+++ b/src/Symfony/Component/HttpClient/AmpHttpClient.php
@@ -33,7 +33,7 @@
use Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(DelegateHttpClient::class)) {
- throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".');
+ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^5".');
}
if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) {
@@ -78,6 +78,9 @@ public function __construct(array $defaultOptions = [], ?callable $clientConfigu
if (is_subclass_of(Request::class, HttpMessage::class)) {
$this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
} else {
+ if (\PHP_VERSION_ID >= 80400) {
+ trigger_deprecation('symfony/http-client', '7.4', 'Using amphp/http-client < 5 is deprecated. Try running "composer require amphp/http-client:^5".');
+ }
$this->multi = new AmpClientStateV4($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
}
}
diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md
index 40dc2ec5d5445..8a44989783c8d 100644
--- a/src/Symfony/Component/HttpClient/CHANGELOG.md
+++ b/src/Symfony/Component/HttpClient/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Deprecate using amphp/http-client < 5
+
7.3
---
diff --git a/src/Symfony/Component/HttpClient/HttpClient.php b/src/Symfony/Component/HttpClient/HttpClient.php
index 3eb3665614fd7..27659358bce4c 100644
--- a/src/Symfony/Component/HttpClient/HttpClient.php
+++ b/src/Symfony/Component/HttpClient/HttpClient.php
@@ -62,7 +62,7 @@ public static function create(array $defaultOptions = [], int $maxHostConnection
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
- @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
+ @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^5" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
return new NativeHttpClient($defaultOptions, $maxHostConnections);
}
diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
index 05d2a7c22870e..8af4c755833bd 100644
--- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
+++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
@@ -50,7 +50,7 @@ public function __construct(int $maxHostConnections, int $maxPendingPushes)
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
- $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
+ $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? min(50 * $maxHostConnections, 4294967295) : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 7ca008fd01f13..7d9e7dd287028 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -31,21 +31,21 @@
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
- "amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/rate-limiter": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0"
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"conflict": {
"amphp/amp": "<2.5",
+ "amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
index 374c31889df3c..ca58a4032d8b8 100644
--- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md
+++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
+
7.3
---
diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php
index 9f421525dacd5..dba930a242672 100644
--- a/src/Symfony/Component/HttpFoundation/Request.php
+++ b/src/Symfony/Component/HttpFoundation/Request.php
@@ -1384,7 +1384,7 @@ public function isMethodCacheable(): bool
public function getProtocolVersion(): ?string
{
if ($this->isFromTrustedProxy()) {
- preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches);
+ preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $this->headers->get('Via') ?? '', $matches);
if ($matches) {
return 'HTTP/'.$matches[2];
diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php
index 6766f2c77099e..173ee3f93eb3b 100644
--- a/src/Symfony/Component/HttpFoundation/Response.php
+++ b/src/Symfony/Component/HttpFoundation/Response.php
@@ -261,7 +261,7 @@ public function prepare(Request $request): static
}
// Fix Content-Type
- $charset = $this->charset ?: 'UTF-8';
+ $charset = $this->charset ?: 'utf-8';
if (!$headers->has('Content-Type')) {
$headers->set('Content-Type', 'text/html; charset='.$charset);
} elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) {
@@ -319,7 +319,8 @@ public function sendHeaders(?int $statusCode = null): static
if (headers_sent()) {
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
$statusCode ??= $this->statusCode;
- header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
+ trigger_deprecation('symfony/http-foundation', '7.4', 'Trying to use "%s::sendHeaders()" after headers have already been sent is deprecated will throw a PHP warning in 8.0. Use a "StreamedResponse" instead.', static::class);
+ //header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
}
return $this;
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
index 84c8629db039c..5cfb980a7b43b 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
@@ -2417,6 +2417,8 @@ public static function protocolVersionProvider()
'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'],
'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'],
'trusted with partially-broken via' => ['HTTP/2.0', true, '1.0 fred, foo', 'HTTP/1.0'],
+ 'trusted with simple via' => ['HTTP/2.0', true, 'HTTP/1.0', 'HTTP/1.0'],
+ 'trusted with only version via' => ['HTTP/2.0', true, '1.0', 'HTTP/1.0'],
];
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
index 2c761a4f8ad17..26ce83df1c48d 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
@@ -63,7 +63,7 @@ public function testSend()
public function testGetCharset()
{
$response = new Response();
- $charsetOrigin = 'UTF-8';
+ $charsetOrigin = 'utf-8';
$response->setCharset($charsetOrigin);
$charset = $response->getCharset();
$this->assertEquals($charsetOrigin, $charset);
@@ -534,7 +534,7 @@ public function testDefaultContentType()
$response = new Response('foo');
$response->prepare(new Request());
- $this->assertSame('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
+ $this->assertSame('text/html; charset=utf-8', $response->headers->get('Content-Type'));
}
public function testContentTypeCharset()
@@ -545,7 +545,7 @@ public function testContentTypeCharset()
// force fixContentType() to be called
$response->prepare(new Request());
- $this->assertEquals('text/css; charset=UTF-8', $response->headers->get('Content-Type'));
+ $this->assertEquals('text/css; charset=utf-8', $response->headers->get('Content-Type'));
}
public function testContentTypeIsNull()
@@ -565,7 +565,7 @@ public function testPrepareDoesNothingIfContentTypeIsSet()
$response->prepare(new Request());
- $this->assertEquals('text/plain; charset=UTF-8', $response->headers->get('content-type'));
+ $this->assertEquals('text/plain; charset=utf-8', $response->headers->get('content-type'));
}
public function testPrepareDoesNothingIfRequestFormatIsNotDefined()
@@ -574,7 +574,7 @@ public function testPrepareDoesNothingIfRequestFormatIsNotDefined()
$response->prepare(new Request());
- $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('content-type'));
+ $this->assertEquals('text/html; charset=utf-8', $response->headers->get('content-type'));
}
/**
@@ -588,7 +588,7 @@ public function testPrepareDoesNotSetContentTypeBasedOnRequestAcceptHeader()
$request->headers->set('Accept', 'application/json');
$response->prepare($request);
- $this->assertSame('text/html; charset=UTF-8', $response->headers->get('content-type'));
+ $this->assertSame('text/html; charset=utf-8', $response->headers->get('content-type'));
}
public function testPrepareSetContentType()
@@ -1021,7 +1021,7 @@ public function testSettersAreChainable()
$setters = [
'setProtocolVersion' => '1.0',
- 'setCharset' => 'UTF-8',
+ 'setCharset' => 'utf-8',
'setPublic' => null,
'setPrivate' => null,
'setDate' => $this->createDateTimeNow(),
diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json
index a86b21b7c728a..9fe4c1a4ba44a 100644
--- a/src/Symfony/Component/HttpFoundation/composer.json
+++ b/src/Symfony/Component/HttpFoundation/composer.json
@@ -24,13 +24,13 @@
"require-dev": {
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
- "symfony/cache": "^6.4.12|^7.1.5",
- "symfony/clock": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/rate-limiter": "^6.4|^7.0"
+ "symfony/cache": "^6.4.12|^7.1.5|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0"
},
"conflict": {
"doctrine/dbal": "<3.6",
diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md
index 6bf1a60ebc6e2..5df71549449f3 100644
--- a/src/Symfony/Component/HttpKernel/CHANGELOG.md
+++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md
@@ -4,11 +4,12 @@ CHANGELOG
7.3
---
+ * Record a `waiting` trace in the `HttpCache` when the cache had to wait for another request to finish
* Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving
* Support `Uid` in `#[MapQueryParameter]`
* Add `ServicesResetterInterface`, implemented by `ServicesResetter`
* Allow configuring the logging channel per type of exceptions in ErrorListener
-
+
7.2
---
diff --git a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
index e913edf9e538a..436e031bbbcac 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
@@ -165,7 +165,6 @@ public function onKernelResponse(ResponseEvent $event): void
}
if (true === $cache->noStore) {
- $response->setPrivate();
$response->headers->addCacheControlDirective('no-store');
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
index 18e8bff4413d8..2599b27de0c97 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
@@ -161,15 +161,15 @@ public static function getSubscribedEvents(): array
/**
* Logs an exception.
- *
+ *
* @param ?string $logChannel
*/
protected function logException(\Throwable $exception, string $message, ?string $logLevel = null, /* ?string $logChannel = null */): void
{
$logChannel = (3 < \func_num_args() ? \func_get_arg(3) : null) ?? $this->resolveLogChannel($exception);
-
+
$logLevel ??= $this->resolveLogLevel($exception);
-
+
if(!$logger = $this->getLogger($logChannel)) {
return;
}
@@ -218,7 +218,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
$attributes = [
'_controller' => $this->controller,
'exception' => $exception,
- 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($exception)),
+ 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($this->resolveLogChannel($exception))),
];
$request = $request->duplicate(null, null, $attributes);
$request->setMethod('GET');
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
new file mode 100644
index 0000000000000..f13946ad71a68
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\HttpCache;
+
+/**
+ * @internal
+ */
+class CacheWasLockedException extends \Exception
+{
+}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 3ef1b8dcb821f..7b6d4c03cb5a9 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -210,7 +210,13 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
$this->record($request, 'reload');
$response = $this->fetch($request, $catch);
} else {
- $response = $this->lookup($request, $catch);
+ $response = null;
+ do {
+ try {
+ $response = $this->lookup($request, $catch);
+ } catch (CacheWasLockedException) {
+ }
+ } while (null === $response);
}
$this->restoreResponseBody($request, $response);
@@ -558,17 +564,11 @@ protected function lock(Request $request, Response $entry): bool
return true;
}
+ $this->record($request, 'waiting');
+
// wait for the lock to be released
if ($this->waitForLock($request)) {
- // replace the current entry with the fresh one
- $new = $this->lookup($request);
- $entry->headers = $new->headers;
- $entry->setContent($new->getContent());
- $entry->setStatusCode($new->getStatusCode());
- $entry->setProtocolVersion($new->getProtocolVersion());
- foreach ($new->headers->getCookies() as $cookie) {
- $entry->headers->setCookie($cookie);
- }
+ throw new CacheWasLockedException(); // unwind back to handle(), try again
} else {
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
$entry->setStatusCode(503);
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 566e721bf3bb3..49c6ecbac1cb1 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -73,15 +73,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.3.0-RC1';
- public const VERSION_ID = 70300;
+ public const VERSION = '7.4.0-DEV';
+ public const VERSION_ID = 70400;
public const MAJOR_VERSION = 7;
- public const MINOR_VERSION = 3;
+ public const MINOR_VERSION = 4;
public const RELEASE_VERSION = 0;
- public const EXTRA_VERSION = 'RC1';
+ public const EXTRA_VERSION = 'DEV';
- public const END_OF_MAINTENANCE = '05/2025';
- public const END_OF_LIFE = '01/2026';
+ public const END_OF_MAINTENANCE = '11/2028';
+ public const END_OF_LIFE = '11/2029';
public function __construct(
protected string $environment,
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
index b185ea8994b1f..d2c8ed0db63d5 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
@@ -102,18 +102,18 @@ public function testResponseIsPublicIfConfigurationIsPublicTrueNoStoreFalse()
$this->assertFalse($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateIfConfigurationIsPublicTrueNoStoreTrue()
+ public function testResponseKeepPublicIfConfigurationIsPublicTrueNoStoreTrue()
{
$request = $this->createRequest(new Cache(public: true, noStore: true));
$this->listener->onKernelResponse($this->createEventMock($request, $this->response));
- $this->assertFalse($this->response->headers->hasCacheControlDirective('public'));
- $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('public'));
+ $this->assertFalse($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue()
+ public function testResponseKeepPrivateNoStoreIfConfigurationIsNoStoreTrue()
{
$request = $this->createRequest(new Cache(noStore: true));
@@ -124,14 +124,14 @@ public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue()
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateIfSharedMaxAgeSetAndNoStoreIsTrue()
+ public function testResponseIsPublicIfSharedMaxAgeSetAndNoStoreIsTrue()
{
$request = $this->createRequest(new Cache(smaxage: 1, noStore: true));
$this->listener->onKernelResponse($this->createEventMock($request, $this->response));
- $this->assertFalse($this->response->headers->hasCacheControlDirective('public'));
- $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('public'));
+ $this->assertFalse($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
index e82e8fd81b481..0f2273c2546b8 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
+use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;
@@ -662,6 +663,7 @@ public function testDegradationWhenCacheLocked()
*/
sleep(10);
+ $this->store = $this->createStore(); // create another store instance that does not hold the current lock
$this->request('GET', '/');
$this->assertHttpKernelIsNotCalled();
$this->assertEquals(200, $this->response->getStatusCode());
@@ -680,6 +682,90 @@ public function testDegradationWhenCacheLocked()
$this->assertEquals('Old response', $this->response->getContent());
}
+ public function testHitBackendOnlyOnceWhenCacheWasLocked()
+ {
+ // Disable stale-while-revalidate, it circumvents waiting for the lock
+ $this->cacheConfig['stale_while_revalidate'] = 0;
+
+ $this->setNextResponses([
+ [
+ 'status' => 200,
+ 'body' => 'initial response',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 304,
+ 'body' => '',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 500,
+ 'body' => 'The backend should not be called twice during revalidation',
+ 'headers' => [],
+ ],
+ ]);
+
+ $this->request('GET', '/'); // warm the cache
+
+ // Use a store that simulates a cache entry being locked upon first attempt
+ $this->store = new class(sys_get_temp_dir() . '/http_cache') extends Store {
+ private bool $hasLock = false;
+
+ public function lock(Request $request): bool
+ {
+ $hasLock = $this->hasLock;
+ $this->hasLock = true;
+
+ return $hasLock;
+ }
+
+ public function isLocked(Request $request): bool
+ {
+ return false;
+ }
+ };
+
+ $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block
+
+ $this->assertEquals(200, $this->response->getStatusCode());
+ $this->assertEquals('initial response', $this->response->getContent());
+
+ $traces = $this->cache->getTraces();
+ $this->assertSame(['waiting', 'stale', 'valid', 'store'], current($traces));
+ }
+
+ public function testTraceAddedWhenCacheLocked()
+ {
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Skips on windows to avoid permissions issues.');
+ }
+
+ // Disable stale-while-revalidate, which circumvents blocking access
+ $this->cacheConfig['stale_while_revalidate'] = 0;
+
+ // The presence of Last-Modified makes this cacheable
+ $this->setNextResponse(200, ['Cache-Control' => 'public, no-cache', 'Last-Modified' => 'some while ago'], 'Old response');
+ $this->request('GET', '/'); // warm the cache
+
+ $primedStore = $this->store;
+
+ $this->store = $this->createMock(Store::class);
+ $this->store->method('lookup')->willReturnCallback(fn (Request $request) => $primedStore->lookup($request));
+ // Assume the cache is locked at the first attempt, but immediately treat the lock as released afterwards
+ $this->store->method('lock')->willReturnOnConsecutiveCalls(false, true);
+ $this->store->method('isLocked')->willReturn(false);
+
+ $this->request('GET', '/');
+
+ $this->assertTraceContains('waiting');
+ }
+
public function testHitsCachedResponseWithSMaxAgeDirective()
{
$time = \DateTimeImmutable::createFromFormat('U', time() - 5);
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
index 26a29f16b2b75..b05d9136661c3 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
@@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase
protected $responses;
protected $catch;
protected $esi;
- protected Store $store;
+ protected ?Store $store = null;
protected function setUp(): void
{
@@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi =
$this->kernel->reset();
- $this->store = new Store(sys_get_temp_dir().'/http_cache');
+ if (!$this->store) {
+ $this->store = $this->createStore();
+ }
if (!isset($this->cacheConfig['debug'])) {
$this->cacheConfig['debug'] = true;
@@ -183,4 +185,9 @@ public static function clearDirectory($directory)
closedir($fp);
}
+
+ protected function createStore(): Store
+ {
+ return new Store(sys_get_temp_dir().'/http_cache');
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json
index bb9f4ba6175de..e3a8b9657d9e7 100644
--- a/src/Symfony/Component/HttpKernel/composer.json
+++ b/src/Symfony/Component/HttpKernel/composer.json
@@ -18,34 +18,34 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/event-dispatcher": "^7.3",
- "symfony/http-foundation": "^7.3",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^7.3|^8.0",
+ "symfony/http-foundation": "^7.3|^8.0",
"symfony/polyfill-ctype": "^1.8",
"psr/log": "^1|^2|^3"
},
"require-dev": {
- "symfony/browser-kit": "^6.4|^7.0",
- "symfony/clock": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/css-selector": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/dom-crawler": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^7.1",
- "symfony/routing": "^6.4|^7.0",
- "symfony/serializer": "^7.1",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^7.1|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^7.1|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3",
- "symfony/uid": "^6.4|^7.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0",
"psr/cache": "^1.0|^2.0|^3.0",
"twig/twig": "^3.12"
},
diff --git a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
index 745e074157974..5d484edacc1b7 100644
--- a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
+++ b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
@@ -160,7 +160,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
$alpha3ToAlpha2 = array_flip($alpha2ToAlpha3);
asort($alpha3ToAlpha2);
- $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping($metadataBundle);
+ $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping(array_flip($this->regionCodes), $metadataBundle);
$numericToAlpha2 = [];
foreach ($alpha2ToNumeric as $alpha2 => $numeric) {
// Add underscore prefix to force keys with leading zeros to remain as string keys.
@@ -231,7 +231,7 @@ private function generateAlpha2ToAlpha3Mapping(array $countries, ArrayAccessible
return $alpha2ToAlpha3;
}
- private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $metadataBundle): array
+ private function generateAlpha2ToNumericMapping(array $countries, ArrayAccessibleResourceBundle $metadataBundle): array
{
$aliases = iterator_to_array($metadataBundle['alias']['territory']);
@@ -250,6 +250,10 @@ private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $m
continue;
}
+ if (!isset($countries[$data['replacement']])) {
+ continue;
+ }
+
if ('deprecated' === $data['reason']) {
continue;
}
diff --git a/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
new file mode 100644
index 0000000000000..dc28340678e53
--- /dev/null
+++ b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
@@ -0,0 +1,10 @@
+ [
+ 'NOK' => [
+ 'kr',
+ 'Norwegian Krone',
+ ],
+ ],
+];
diff --git a/src/Symfony/Component/Intl/Resources/data/regions/meta.php b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
index 1c9f233273af7..e0a99ccb7f5a8 100644
--- a/src/Symfony/Component/Intl/Resources/data/regions/meta.php
+++ b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
@@ -755,7 +755,6 @@
'ZWE' => 'ZW',
],
'Alpha2ToNumeric' => [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -943,18 +942,6 @@
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -1012,29 +999,6 @@
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -1042,7 +1006,6 @@
'ZW' => '716',
],
'NumericToAlpha2' => [
- '_958' => 'AA',
'_020' => 'AD',
'_784' => 'AE',
'_004' => 'AF',
@@ -1230,18 +1193,6 @@
'_585' => 'PW',
'_600' => 'PY',
'_634' => 'QA',
- '_959' => 'QM',
- '_960' => 'QN',
- '_962' => 'QP',
- '_963' => 'QQ',
- '_964' => 'QR',
- '_965' => 'QS',
- '_966' => 'QT',
- '_968' => 'QV',
- '_969' => 'QW',
- '_970' => 'QX',
- '_971' => 'QY',
- '_972' => 'QZ',
'_638' => 'RE',
'_642' => 'RO',
'_688' => 'RS',
@@ -1299,29 +1250,6 @@
'_548' => 'VU',
'_876' => 'WF',
'_882' => 'WS',
- '_975' => 'XC',
- '_976' => 'XD',
- '_977' => 'XE',
- '_978' => 'XF',
- '_979' => 'XG',
- '_980' => 'XH',
- '_981' => 'XI',
- '_982' => 'XJ',
- '_984' => 'XL',
- '_985' => 'XM',
- '_986' => 'XN',
- '_987' => 'XO',
- '_988' => 'XP',
- '_989' => 'XQ',
- '_990' => 'XR',
- '_991' => 'XS',
- '_992' => 'XT',
- '_993' => 'XU',
- '_994' => 'XV',
- '_995' => 'XW',
- '_996' => 'XX',
- '_997' => 'XY',
- '_998' => 'XZ',
'_887' => 'YE',
'_175' => 'YT',
'_710' => 'ZA',
diff --git a/src/Symfony/Component/Intl/Tests/CountriesTest.php b/src/Symfony/Component/Intl/Tests/CountriesTest.php
index 7b921036b2a00..01f0f76f2e40a 100644
--- a/src/Symfony/Component/Intl/Tests/CountriesTest.php
+++ b/src/Symfony/Component/Intl/Tests/CountriesTest.php
@@ -527,7 +527,6 @@ class CountriesTest extends ResourceBundleTestCase
];
private const ALPHA2_TO_NUMERIC = [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -715,18 +714,6 @@ class CountriesTest extends ResourceBundleTestCase
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -784,29 +771,6 @@ class CountriesTest extends ResourceBundleTestCase
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -814,6 +778,19 @@ class CountriesTest extends ResourceBundleTestCase
'ZW' => '716',
];
+ public function testAllGettersGenerateTheSameDataSetCount()
+ {
+ $alpha2Count = count(Countries::getCountryCodes());
+ $alpha3Count = count(Countries::getAlpha3Codes());
+ $numericCodesCount = count(Countries::getNumericCodes());
+ $namesCount = count(Countries::getNames());
+
+ // we base all on Name count since it is the first to be generated
+ $this->assertEquals($namesCount, $alpha2Count, 'Alpha 2 count does not match');
+ $this->assertEquals($namesCount, $alpha3Count, 'Alpha 3 count does not match');
+ $this->assertEquals($namesCount, $numericCodesCount, 'Numeric codes count does not match');
+ }
+
public function testGetCountryCodes()
{
$this->assertSame(self::COUNTRIES, Countries::getCountryCodes());
@@ -992,7 +969,7 @@ public function testGetNumericCode()
public function testNumericCodeExists()
{
$this->assertTrue(Countries::numericCodeExists('250'));
- $this->assertTrue(Countries::numericCodeExists('982'));
+ $this->assertTrue(Countries::numericCodeExists('008'));
$this->assertTrue(Countries::numericCodeExists('716'));
$this->assertTrue(Countries::numericCodeExists('036'));
$this->assertFalse(Countries::numericCodeExists('667'));
diff --git a/src/Symfony/Component/Intl/composer.json b/src/Symfony/Component/Intl/composer.json
index b2101cfe5f728..34a948bc0a621 100644
--- a/src/Symfony/Component/Intl/composer.json
+++ b/src/Symfony/Component/Intl/composer.json
@@ -28,8 +28,8 @@
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/filesystem": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/string": "<7.1"
diff --git a/src/Symfony/Component/JsonPath/JsonCrawler.php b/src/Symfony/Component/JsonPath/JsonCrawler.php
index 75c61e14f79d7..0793a5c5d7b14 100644
--- a/src/Symfony/Component/JsonPath/JsonCrawler.php
+++ b/src/Symfony/Component/JsonPath/JsonCrawler.php
@@ -80,19 +80,7 @@ private function evaluate(JsonPath $query): array
throw new InvalidJsonStringInputException($e->getMessage(), $e);
}
- $current = [$data];
-
- foreach ($tokens as $token) {
- $next = [];
- foreach ($current as $value) {
- $result = $this->evaluateToken($token, $value);
- $next = array_merge($next, $result);
- }
-
- $current = $next;
- }
-
- return $current;
+ return $this->evaluateTokensOnDecodedData($tokens, $data);
} catch (InvalidArgumentException $e) {
throw $e;
} catch (\Throwable $e) {
@@ -100,6 +88,23 @@ private function evaluate(JsonPath $query): array
}
}
+ private function evaluateTokensOnDecodedData(array $tokens, array $data): array
+ {
+ $current = [$data];
+
+ foreach ($tokens as $token) {
+ $next = [];
+ foreach ($current as $value) {
+ $result = $this->evaluateToken($token, $value);
+ $next = array_merge($next, $result);
+ }
+
+ $current = $next;
+ }
+
+ return $current;
+ }
+
private function evaluateToken(JsonPathToken $token, mixed $value): array
{
return match ($token->type) {
@@ -128,7 +133,11 @@ private function evaluateBracket(string $expr, mixed $value): array
return [];
}
- if ('*' === $expr) {
+ if (str_contains($expr, ',') && (str_starts_with($trimmed = trim($expr), ',') || str_ends_with($trimmed, ','))) {
+ throw new JsonCrawlerException($expr, 'Expression cannot have leading or trailing commas');
+ }
+
+ if ('*' === $expr = JsonPathUtils::normalizeWhitespace($expr)) {
return array_values($value);
}
@@ -163,8 +172,7 @@ private function evaluateBracket(string $expr, mixed $value): array
return $result;
}
- // start, end and step
- if (preg_match('/^(-?\d*):(-?\d*)(?::(-?\d+))?$/', $expr, $matches)) {
+ if (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d++))?$/', $expr, $matches)) {
if (!array_is_list($value)) {
return [];
}
@@ -212,25 +220,72 @@ private function evaluateBracket(string $expr, mixed $value): array
// filter expressions
if (preg_match('/^\?(.*)$/', $expr, $matches)) {
- $filterExpr = $matches[1];
-
- if (preg_match('/^(\w+)\s*\([^()]*\)\s*([<>=!]+.*)?$/', $filterExpr)) {
+ if (preg_match('/^(\w+)\s*\([^()]*\)\s*([<>=!]+.*)?$/', $filterExpr = trim($matches[1]))) {
$filterExpr = "($filterExpr)";
}
if (!str_starts_with($filterExpr, '(')) {
- throw new JsonCrawlerException($expr, 'Invalid filter expression');
+ $filterExpr = "($filterExpr)";
}
- // remove outrer filter parentheses
+ // remove outer filter parentheses
$innerExpr = substr(substr($filterExpr, 1), 0, -1);
return $this->evaluateFilter($innerExpr, $value);
}
- // quoted strings for object keys
+ // comma-separated values, e.g. `['key1', 'key2', 123]` or `[0, 1, 'key']`
+ if (str_contains($expr, ',')) {
+ $parts = JsonPathUtils::parseCommaSeparatedValues($expr);
+
+ $result = [];
+
+ foreach ($parts as $part) {
+ $part = trim($part);
+
+ if ('*' === $part) {
+ $result = array_merge($result, array_values($value));
+ } elseif (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d++))?$/', $part, $matches)) {
+ // slice notation
+ $sliceResult = $this->evaluateBracket($part, $value);
+ $result = array_merge($result, $sliceResult);
+ } elseif (preg_match('/^([\'"])(.*)\1$/', $part, $matches)) {
+ $key = JsonPathUtils::unescapeString($matches[2], $matches[1]);
+
+ if (array_is_list($value)) {
+ // for arrays, find ALL objects that contain this key
+ foreach ($value as $item) {
+ if (\is_array($item) && \array_key_exists($key, $item)) {
+ $result[] = $item;
+ }
+ }
+ } elseif (\array_key_exists($key, $value)) { // for objects, get the value for this key
+ $result[] = $value[$key];
+ }
+ } elseif (preg_match('/^-?\d+$/', $part)) {
+ // numeric index
+ $index = (int) $part;
+ if ($index < 0) {
+ $index = \count($value) + $index;
+ }
+
+ if (array_is_list($value) && \array_key_exists($index, $value)) {
+ $result[] = $value[$index];
+ } else {
+ // numeric index on a hashmap
+ $keysIndices = array_keys($value);
+ if (isset($keysIndices[$index]) && isset($value[$keysIndices[$index]])) {
+ $result[] = $value[$keysIndices[$index]];
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
if (preg_match('/^([\'"])(.*)\1$/', $expr, $matches)) {
- $key = stripslashes($matches[2]);
+ $key = JsonPathUtils::unescapeString($matches[2], $matches[1]);
return \array_key_exists($key, $value) ? [$value[$key]] : [];
}
@@ -246,10 +301,6 @@ private function evaluateFilter(string $expr, mixed $value): array
$result = [];
foreach ($value as $item) {
- if (!\is_array($item)) {
- continue;
- }
-
if ($this->evaluateFilterExpression($expr, $item)) {
$result[] = $item;
}
@@ -258,9 +309,31 @@ private function evaluateFilter(string $expr, mixed $value): array
return $result;
}
- private function evaluateFilterExpression(string $expr, array $context): bool
+ private function evaluateFilterExpression(string $expr, mixed $context): bool
{
- $expr = trim($expr);
+ $expr = JsonPathUtils::normalizeWhitespace($expr);
+
+ // remove outer parentheses if they wrap the entire expression
+ if (str_starts_with($expr, '(') && str_ends_with($expr, ')')) {
+ $depth = 0;
+ $isWrapped = true;
+ $i = -1;
+ while (null !== $char = $expr[++$i] ?? null) {
+ if ('(' === $char) {
+ ++$depth;
+ } elseif (')' === $char && 0 === --$depth && isset($expr[$i + 1])) {
+ $isWrapped = false;
+ break;
+ }
+ }
+ if ($isWrapped) {
+ $expr = trim(substr($expr, 1, -1));
+ }
+ }
+
+ if (str_starts_with($expr, '!')) {
+ return !$this->evaluateFilterExpression(trim(substr($expr, 1)), $context);
+ }
if (str_contains($expr, '&&')) {
$parts = array_map('trim', explode('&&', $expr));
@@ -294,15 +367,17 @@ private function evaluateFilterExpression(string $expr, array $context): bool
}
}
- if (str_starts_with($expr, '@.')) {
- $path = substr($expr, 2);
+ if ('@' === $expr) {
+ return true;
+ }
- return \array_key_exists($path, $context);
+ if (str_starts_with($expr, '@.')) {
+ return (bool) ($this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? false);
}
// function calls
- if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
- $functionName = $matches[1];
+ if (preg_match('/^(\w++)\s*+\((.*)\)$/', $expr, $matches)) {
+ $functionName = trim($matches[1]);
if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
}
@@ -315,10 +390,21 @@ private function evaluateFilterExpression(string $expr, array $context): bool
return false;
}
- private function evaluateScalar(string $expr, array $context): mixed
+ private function evaluateScalar(string $expr, mixed $context): mixed
{
- if (is_numeric($expr)) {
- return str_contains($expr, '.') ? (float) $expr : (int) $expr;
+ $expr = JsonPathUtils::normalizeWhitespace($expr);
+
+ if (JsonPathUtils::isJsonNumber($expr)) {
+ return str_contains($expr, '.') || str_contains(strtolower($expr), 'e') ? (float) $expr : (int) $expr;
+ }
+
+ // only validate tokens that look like standalone numbers
+ if (preg_match('/^[\d+\-.eE]+$/', $expr) && preg_match('/\d/', $expr)) {
+ throw new JsonCrawlerException($expr, \sprintf('Invalid number format "%s"', $expr));
+ }
+
+ if ('@' === $expr) {
+ return $context;
}
if ('true' === $expr) {
@@ -335,20 +421,21 @@ private function evaluateScalar(string $expr, array $context): mixed
// string literals
if (preg_match('/^([\'"])(.*)\1$/', $expr, $matches)) {
- return $matches[2];
+ return JsonPathUtils::unescapeString($matches[2], $matches[1]);
}
// current node references
- if (str_starts_with($expr, '@.')) {
- $path = substr($expr, 2);
+ if (str_starts_with($expr, '@')) {
+ if (!\is_array($context)) {
+ return null;
+ }
- return $context[$path] ?? null;
+ return $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? null;
}
// function calls
- if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
- $functionName = $matches[1];
- if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
+ if (preg_match('/^(\w++)\((.*)\)$/', $expr, $matches)) {
+ if (!isset(self::RFC9535_FUNCTIONS[$functionName = trim($matches[1])])) {
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
}
@@ -358,14 +445,43 @@ private function evaluateScalar(string $expr, array $context): mixed
return null;
}
- private function evaluateFunction(string $name, string $args, array $context): mixed
+ private function evaluateFunction(string $name, string $args, mixed $context): mixed
{
- $args = array_map(
- fn ($arg) => $this->evaluateScalar(trim($arg), $context),
- explode(',', $args)
- );
+ $argList = [];
+ $nodelistSizes = [];
+ if ($args = trim($args)) {
+ $args = JsonPathUtils::parseCommaSeparatedValues($args);
+ foreach ($args as $arg) {
+ $arg = trim($arg);
+ if (str_starts_with($arg, '$')) { // special handling for absolute paths
+ $results = $this->evaluate(new JsonPath($arg));
+ $argList[] = $results[0] ?? null;
+ $nodelistSizes[] = \count($results);
+ } elseif (!str_starts_with($arg, '@')) { // special handling for @ to track nodelist size
+ $argList[] = $this->evaluateScalar($arg, $context);
+ $nodelistSizes[] = 1;
+ } elseif ('@' === $arg) {
+ $argList[] = $context;
+ $nodelistSizes[] = 1;
+ } elseif (!\is_array($context)) {
+ $argList[] = null;
+ $nodelistSizes[] = 0;
+ } elseif (str_starts_with($pathPart = substr($arg, 1), '[')) {
+ // handle bracket expressions like @['a','d']
+ $results = $this->evaluateBracket(substr($pathPart, 1, -1), $context);
+ $argList[] = $results;
+ $nodelistSizes[] = \count($results);
+ } else {
+ // handle dot notation like @.a
+ $results = $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.$pathPart)), $context);
+ $argList[] = $results[0] ?? null;
+ $nodelistSizes[] = \count($results);
+ }
+ }
+ }
- $value = $args[0] ?? null;
+ $value = $argList[0] ?? null;
+ $nodelistSize = $nodelistSizes[0] ?? 0;
return match ($name) {
'length' => match (true) {
@@ -373,16 +489,16 @@ private function evaluateFunction(string $name, string $args, array $context): m
\is_array($value) => \count($value),
default => 0,
},
- 'count' => \is_array($value) ? \count($value) : 0,
+ 'count' => $nodelistSize,
'match' => match (true) {
- \is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/', $args[1]), $value),
+ \is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/u', $this->transformJsonPathRegex($argList[1])), $value),
default => false,
},
'search' => match (true) {
- \is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match("/$args[1]/", $value),
+ \is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match("/{$this->transformJsonPathRegex($argList[1])}/u", $value),
default => false,
},
- 'value' => $value,
+ 'value' => 1 < $nodelistSize ? null : (1 === $nodelistSize ? (\is_array($value) ? ($value[0] ?? null) : $value) : $value),
default => null,
};
}
@@ -415,4 +531,52 @@ private function compare(mixed $left, mixed $right, string $operator): bool
default => false,
};
}
+
+ /**
+ * Transforms JSONPath regex patterns to comply with RFC 9535.
+ *
+ * The main issue is that '.' should not match \r or \n but should
+ * match Unicode line separators U+2028 and U+2029.
+ */
+ private function transformJsonPathRegex(string $pattern): string
+ {
+ $result = '';
+ $inCharClass = false;
+ $escaped = false;
+ $i = -1;
+
+ while (null !== $char = $pattern[++$i] ?? null) {
+ if ($escaped) {
+ $result .= $char;
+ $escaped = false;
+ continue;
+ }
+
+ if ('\\' === $char) {
+ $result .= $char;
+ $escaped = true;
+ continue;
+ }
+
+ if ('[' === $char && !$inCharClass) {
+ $inCharClass = true;
+ $result .= $char;
+ continue;
+ }
+
+ if (']' === $char && $inCharClass) {
+ $inCharClass = false;
+ $result .= $char;
+ continue;
+ }
+
+ if ('.' === $char && !$inCharClass) {
+ $result .= '(?:[^\r\n]|\x{2028}|\x{2029})';
+ } else {
+ $result .= $char;
+ }
+ }
+
+ return $result;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php b/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
index 3e8a222f0ba8e..4859c2bde076b 100644
--- a/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
+++ b/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
@@ -25,7 +25,7 @@ interface JsonCrawlerInterface
* @return list
*
* @throws InvalidArgumentException When the JSON string provided to the crawler cannot be decoded
- * @throws JsonCrawlerException When a syntax error occurs in the provided JSON path
+ * @throws JsonCrawlerException When a syntax error occurs in the provided JSON path
*/
public function find(string|JsonPath $query): array;
}
diff --git a/src/Symfony/Component/JsonPath/JsonPath.php b/src/Symfony/Component/JsonPath/JsonPath.php
index 1009369b0a56d..e36fc9ffd2ef1 100644
--- a/src/Symfony/Component/JsonPath/JsonPath.php
+++ b/src/Symfony/Component/JsonPath/JsonPath.php
@@ -30,7 +30,9 @@ public function __construct(
public function key(string $key): static
{
- return new self($this->path.(str_ends_with($this->path, '..') ? '' : '.').$key);
+ $escaped = $this->escapeKey($key);
+
+ return new self($this->path.'["'.$escaped.'"]');
}
public function index(int $index): static
@@ -80,4 +82,25 @@ public function __toString(): string
{
return $this->path;
}
+
+ private function escapeKey(string $key): string
+ {
+ $key = strtr($key, [
+ '\\' => '\\\\',
+ '"' => '\\"',
+ "\n" => '\\n',
+ "\r" => '\\r',
+ "\t" => '\\t',
+ "\b" => '\\b',
+ "\f" => '\\f',
+ ]);
+
+ for ($i = 0; $i <= 31; ++$i) {
+ if ($i < 8 || $i > 13) {
+ $key = str_replace(\chr($i), \sprintf('\\u%04x', $i), $key);
+ }
+ }
+
+ return $key;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/JsonPathUtils.php b/src/Symfony/Component/JsonPath/JsonPathUtils.php
index b5ac2ae6b8d0a..30bf446b6a9d5 100644
--- a/src/Symfony/Component/JsonPath/JsonPathUtils.php
+++ b/src/Symfony/Component/JsonPath/JsonPathUtils.php
@@ -85,4 +85,146 @@ public static function findSmallestDeserializableStringAndPath(array $tokens, mi
'tokens' => $remainingTokens,
];
}
+
+ public static function unescapeString(string $str, string $quoteChar): string
+ {
+ if ('"' === $quoteChar) {
+ // try JSON decoding first for unicode sequences
+ $jsonStr = '"'.$str.'"';
+ $decoded = json_decode($jsonStr, true);
+
+ if (null !== $decoded) {
+ return $decoded;
+ }
+ }
+
+ $result = '';
+ $i = -1;
+
+ while (null !== $char = $str[++$i] ?? null) {
+ if ('\\' === $char && isset($str[$i + 1])) {
+ $result .= match ($str[$i + 1]) {
+ '"' => '"',
+ "'" => "'",
+ '\\' => '\\',
+ '/' => '/',
+ 'b' => "\b",
+ 'f' => "\f",
+ 'n' => "\n",
+ 'r' => "\r",
+ 't' => "\t",
+ 'u' => self::unescapeUnicodeSequence($str, $i),
+ default => $char.$str[$i + 1], // keep the backslash
+ };
+
+ ++$i;
+ } else {
+ $result .= $char;
+ }
+ }
+
+ return $result;
+ }
+
+ private static function unescapeUnicodeSequence(string $str, int &$i): string
+ {
+ if (!isset($str[$i + 5])) {
+ // not enough characters for Unicode escape, treat as literal
+ return $str[$i];
+ }
+
+ $hex = substr($str, $i + 2, 4);
+ if (!ctype_xdigit($hex)) {
+ // invalid hex, treat as literal
+ return $str[$i];
+ }
+
+ $codepoint = hexdec($hex);
+ // looks like a valid Unicode codepoint, string length is sufficient and it starts with \u
+ if (0xD800 <= $codepoint && $codepoint <= 0xDBFF && isset($str[$i + 11]) && '\\' === $str[$i + 6] && 'u' === $str[$i + 7]) {
+ $lowHex = substr($str, $i + 8, 4);
+ if (ctype_xdigit($lowHex)) {
+ $lowSurrogate = hexdec($lowHex);
+ if (0xDC00 <= $lowSurrogate && $lowSurrogate <= 0xDFFF) {
+ $codepoint = 0x10000 + (($codepoint & 0x3FF) << 10) + ($lowSurrogate & 0x3FF);
+ $i += 10; // skip surrogate pair
+
+ return mb_chr($codepoint, 'UTF-8');
+ }
+ }
+ }
+
+ // single Unicode character or invalid surrogate, skip the sequence
+ $i += 4;
+
+ return mb_chr($codepoint, 'UTF-8');
+ }
+
+ /**
+ * @see https://datatracker.ietf.org/doc/rfc9535/, section 2.1.1
+ */
+ public static function normalizeWhitespace(string $input): string
+ {
+ $normalized = strtr($input, [
+ "\t" => ' ',
+ "\n" => ' ',
+ "\r" => ' ',
+ ]);
+
+ return trim($normalized);
+ }
+
+ /**
+ * Check a number is RFC 9535 compliant using strict JSON number format.
+ */
+ public static function isJsonNumber(string $value): bool
+ {
+ return preg_match('/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/', $value);
+ }
+
+ public static function parseCommaSeparatedValues(string $expr): array
+ {
+ $parts = [];
+ $current = '';
+ $inQuotes = false;
+ $quoteChar = null;
+ $bracketDepth = 0;
+ $i = -1;
+
+ while (null !== $char = $expr[++$i] ?? null) {
+ if ('\\' === $char && isset($expr[$i + 1])) {
+ $current .= $char.$expr[++$i];
+ continue;
+ }
+
+ if ('"' === $char || "'" === $char) {
+ if (!$inQuotes) {
+ $inQuotes = true;
+ $quoteChar = $char;
+ } elseif ($char === $quoteChar) {
+ $inQuotes = false;
+ $quoteChar = null;
+ }
+ } elseif (!$inQuotes) {
+ if ('[' === $char) {
+ ++$bracketDepth;
+ } elseif (']' === $char) {
+ --$bracketDepth;
+ } elseif (0 === $bracketDepth && ',' === $char) {
+ $parts[] = trim($current);
+ $current = '';
+
+ continue;
+ }
+ }
+
+ $current .= $char;
+ }
+
+ if ('' !== $current) {
+ $parts[] = trim($current);
+ }
+
+ return $parts;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile
new file mode 100644
index 0000000000000..d9b4c353f4a76
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile
@@ -0,0 +1,9 @@
+override hash := 05f6cac786bf0cce95437e6f1adedc3186d54a71
+
+.PHONY: cts.json
+cts.json:
+ curl -f https://raw.githubusercontent.com/jsonpath-standard/jsonpath-compliance-test-suite/$(hash)/cts.json -o cts.json
+
+.PHONY: clean
+clean:
+ rm -f cts.json
diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json
new file mode 100644
index 0000000000000..363dce7893ca6
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json
@@ -0,0 +1,12702 @@
+{
+ "description": "JSONPath Compliance Test Suite. This file is autogenerated, do not edit.",
+ "tests": [
+ {
+ "name": "basic, root",
+ "selector": "$",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ [
+ "first",
+ "second"
+ ]
+ ],
+ "result_paths": [
+ "$"
+ ]
+ },
+ {
+ "name": "basic, no leading whitespace",
+ "selector": " $",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, no trailing whitespace",
+ "selector": "$ ",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, name shorthand",
+ "selector": "$.a",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, extended unicode ☺",
+ "selector": "$.☺",
+ "document": {
+ "☺": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, underscore",
+ "selector": "$._",
+ "document": {
+ "_": "A",
+ "_foo": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['_']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, symbol",
+ "selector": "$.&",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, name shorthand, number",
+ "selector": "$.1",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, name shorthand, absent data",
+ "selector": "$.c",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "basic, name shorthand, array data",
+ "selector": "$.a",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "basic, name shorthand, object data, nested",
+ "selector": "$.a.b.c",
+ "document": {
+ "a": {
+ "b": {
+ "c": "C"
+ }
+ }
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['a']['b']['c']"
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, object data",
+ "selector": "$.*",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "results": [
+ [
+ "A",
+ "B"
+ ],
+ [
+ "B",
+ "A"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, array data",
+ "selector": "$.*",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first",
+ "second"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, wildcard selector, array data",
+ "selector": "$[*]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first",
+ "second"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, then name shorthand",
+ "selector": "$.*.a",
+ "document": {
+ "x": {
+ "a": "Ax",
+ "b": "Bx"
+ },
+ "y": {
+ "a": "Ay",
+ "b": "By"
+ }
+ },
+ "results": [
+ [
+ "Ax",
+ "Ay"
+ ],
+ [
+ "Ay",
+ "Ax"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['x']['a']",
+ "$['y']['a']"
+ ],
+ [
+ "$['y']['a']",
+ "$['x']['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, multiple selectors",
+ "selector": "$[0,2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 2
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, space instead of comma",
+ "selector": "$[0 2]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, selector, leading comma",
+ "selector": "$[,0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, selector, trailing comma",
+ "selector": "$[0,]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, multiple selectors, name and index, array data",
+ "selector": "$['a',1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, name and index, object data",
+ "selector": "$['a',1]",
+ "document": {
+ "a": 1,
+ "b": 2
+ },
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, index and slice",
+ "selector": "$[1,5:7]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 5,
+ 6
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[5]",
+ "$[6]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, index and slice, overlapping",
+ "selector": "$[1,0:3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 0,
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, duplicate index",
+ "selector": "$[1,1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 1
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and index",
+ "selector": "$[*,1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and name",
+ "selector": "$[*,'a']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "results": [
+ [
+ "A",
+ "B",
+ "A"
+ ],
+ [
+ "B",
+ "A",
+ "A"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and slice",
+ "selector": "$[*,0:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, multiple wildcards",
+ "selector": "$[*,*]",
+ "document": [
+ 0,
+ 1,
+ 2
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 0,
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, empty segment",
+ "selector": "$[]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, descendant segment, index",
+ "selector": "$..[1]",
+ "document": {
+ "o": [
+ 0,
+ 1,
+ [
+ 2,
+ 3
+ ]
+ ]
+ },
+ "result": [
+ 1,
+ 3
+ ],
+ "result_paths": [
+ "$['o'][1]",
+ "$['o'][2][1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, name shorthand",
+ "selector": "$..a",
+ "document": {
+ "o": [
+ {
+ "a": "b"
+ },
+ {
+ "a": "c"
+ }
+ ]
+ },
+ "result": [
+ "b",
+ "c"
+ ],
+ "result_paths": [
+ "$['o'][0]['a']",
+ "$['o'][1]['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, array data",
+ "selector": "$..*",
+ "document": [
+ 0,
+ 1
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, array data",
+ "selector": "$..[*]",
+ "document": [
+ 0,
+ 1
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, nested arrays",
+ "selector": "$..[*]",
+ "document": [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ]
+ ],
+ "results": [
+ [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ],
+ [
+ 1
+ ],
+ 1,
+ 2
+ ],
+ [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ],
+ [
+ 1
+ ],
+ 2,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$[0]",
+ "$[1]",
+ "$[0][0]",
+ "$[0][0][0]",
+ "$[1][0]"
+ ],
+ [
+ "$[0]",
+ "$[1]",
+ "$[0][0]",
+ "$[1][0]",
+ "$[0][0][0]"
+ ]
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, nested objects",
+ "selector": "$..[*]",
+ "document": {
+ "a": {
+ "c": {
+ "e": 1
+ }
+ },
+ "b": {
+ "d": 2
+ }
+ },
+ "results": [
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ {
+ "e": 1
+ },
+ 1,
+ 2
+ ],
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ {
+ "e": 1
+ },
+ 2,
+ 1
+ ],
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ 2,
+ {
+ "e": 1
+ },
+ 1
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "e": 1
+ },
+ 1,
+ 2
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "e": 1
+ },
+ 2,
+ 1
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ 2,
+ {
+ "e": 1
+ },
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']['c']",
+ "$['a']['c']['e']",
+ "$['b']['d']"
+ ],
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']['c']",
+ "$['b']['d']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['a']",
+ "$['b']",
+ "$['b']['d']",
+ "$['a']['c']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']['c']",
+ "$['a']['c']['e']",
+ "$['b']['d']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']['c']",
+ "$['b']['d']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['b']['d']",
+ "$['a']['c']",
+ "$['a']['c']['e']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, object data",
+ "selector": "$..*",
+ "document": {
+ "a": "b"
+ },
+ "result": [
+ "b"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, nested data",
+ "selector": "$..*",
+ "document": {
+ "o": [
+ {
+ "a": "b"
+ }
+ ]
+ },
+ "result": [
+ [
+ {
+ "a": "b"
+ }
+ ],
+ {
+ "a": "b"
+ },
+ "b"
+ ],
+ "result_paths": [
+ "$['o']",
+ "$['o'][0]",
+ "$['o'][0]['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, multiple selectors",
+ "selector": "$..['a','d']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ "b",
+ "e",
+ "c",
+ "f"
+ ],
+ "result_paths": [
+ "$[0]['a']",
+ "$[0]['d']",
+ "$[1]['a']",
+ "$[1]['d']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, object traversal, multiple selectors",
+ "selector": "$..['a','d']",
+ "document": {
+ "x": {
+ "a": "b",
+ "d": "e"
+ },
+ "y": {
+ "a": "c",
+ "d": "f"
+ }
+ },
+ "results": [
+ [
+ "b",
+ "e",
+ "c",
+ "f"
+ ],
+ [
+ "c",
+ "f",
+ "b",
+ "e"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['x']['a']",
+ "$['x']['d']",
+ "$['y']['a']",
+ "$['y']['d']"
+ ],
+ [
+ "$['y']['a']",
+ "$['y']['d']",
+ "$['x']['a']",
+ "$['x']['d']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, bald descendant segment",
+ "selector": "$..",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, current node identifier without filter selector",
+ "selector": "$[@.a]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, root node identifier in brackets without filter selector",
+ "selector": "$[$.a]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, existence, without segments",
+ "selector": "$[?@]",
+ "document": {
+ "a": 1,
+ "b": null
+ },
+ "results": [
+ [
+ 1,
+ null
+ ],
+ [
+ null,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, existence",
+ "selector": "$[?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, existence, present with null",
+ "selector": "$[?@.a]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, absolute existence, without segments",
+ "selector": "$[?$]",
+ "document": {
+ "a": 1,
+ "b": null
+ },
+ "results": [
+ [
+ 1,
+ null
+ ],
+ [
+ null,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, absolute existence, with segments",
+ "selector": "$[?$.*.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, equals string, single quotes",
+ "selector": "$[?@.a=='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals numeric string, single quotes",
+ "selector": "$[?@.a=='1']",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "1",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals string, double quotes",
+ "selector": "$[?@.a==\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals numeric string, double quotes",
+ "selector": "$[?@.a==\"1\"]",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "1",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number",
+ "selector": "$[?@.a==1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals null",
+ "selector": "$[?@.a==null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals null, absent from data",
+ "selector": "$[?@.a==null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, equals true",
+ "selector": "$[?@.a==true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": true,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals false",
+ "selector": "$[?@.a==false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals self",
+ "selector": "$[?@==@]",
+ "document": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, absolute, equals self",
+ "selector": "$[?$==$]",
+ "document": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, equals, absent from index selector equals absent from name selector",
+ "selector": "$[?@.absent==@.list[9]]",
+ "document": [
+ {
+ "list": [
+ 1
+ ]
+ }
+ ],
+ "result": [
+ {
+ "list": [
+ 1
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, deep equality, arrays",
+ "selector": "$[?@.a==@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": [
+ 1,
+ 2
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ [
+ 2
+ ],
+ 1
+ ]
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ 2
+ ]
+ ]
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, deep equality, objects",
+ "selector": "$[?@.a==@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "y": {
+ "z": 1
+ },
+ "x": 1
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 2
+ }
+ }
+ }
+ ],
+ "result": [
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "y": {
+ "z": 1
+ },
+ "x": 1
+ }
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, single quotes",
+ "selector": "$[?@.a!='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals numeric string, single quotes",
+ "selector": "$[?@.a!='1']",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, single quotes, different type",
+ "selector": "$[?@.a!='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, double quotes",
+ "selector": "$[?@.a!=\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals numeric string, double quotes",
+ "selector": "$[?@.a!=\"1\"]",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, double quotes, different types",
+ "selector": "$[?@.a!=\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals number",
+ "selector": "$[?@.a!=1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not-equals number, different types",
+ "selector": "$[?@.a!=1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals null",
+ "selector": "$[?@.a!=null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals null, absent from data",
+ "selector": "$[?@.a!=null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals true",
+ "selector": "$[?@.a!=true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals false",
+ "selector": "$[?@.a!=false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, less than string, single quotes",
+ "selector": "$[?@.a<'c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than string, double quotes",
+ "selector": "$[?@.a<\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than number",
+ "selector": "$[?@.a<10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than null",
+ "selector": "$[?@.a'c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than string, double quotes",
+ "selector": "$[?@.a>\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than number",
+ "selector": "$[?@.a>10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, greater than null",
+ "selector": "$[?@.a>null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than true",
+ "selector": "$[?@.a>true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than false",
+ "selector": "$[?@.a>false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than or equal to string, single quotes",
+ "selector": "$[?@.a>='c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to string, double quotes",
+ "selector": "$[?@.a>=\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to number",
+ "selector": "$[?@.a>=10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to null",
+ "selector": "$[?@.a>=null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to true",
+ "selector": "$[?@.a>=true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": true,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to false",
+ "selector": "$[?@.a>=false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, exists and not-equals null, absent from data",
+ "selector": "$[?@.a&&@.a!=null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, exists and exists, data false",
+ "selector": "$[?@.a&&@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ },
+ {
+ "c": false
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "b": false
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, exists or exists, data false",
+ "selector": "$[?@.a||@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ },
+ {
+ "c": false
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, and",
+ "selector": "$[?@.a>0&&@.a<10]",
+ "document": [
+ {
+ "a": -10,
+ "d": "e"
+ },
+ {
+ "a": 5,
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 5,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, or",
+ "selector": "$[?@.a=='b'||@.a=='d']",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, not expression",
+ "selector": "$[?!(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not exists",
+ "selector": "$[?!@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not exists, data null",
+ "selector": "$[?!@.a]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, wildcard",
+ "selector": "$[?@.*]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ {
+ "a": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, multiple",
+ "selector": "$[?@[0, 0, 'a']]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ [
+ 2,
+ 3
+ ],
+ {
+ "a": 3
+ },
+ {
+ "b": 4
+ },
+ {
+ "a": 3,
+ "b": 4
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ [
+ 2,
+ 3
+ ],
+ {
+ "a": 3
+ },
+ {
+ "a": 3,
+ "b": 4
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[6]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, slice",
+ "selector": "$[?@[0:2]]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ [
+ 2,
+ 3,
+ 4
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ [
+ 2,
+ 3,
+ 4
+ ]
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, negated",
+ "selector": "$[?!@.*]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ 1,
+ [],
+ {}
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, non-singular query in comparison, slice",
+ "selector": "$[?@[0:0]==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, all children",
+ "selector": "$[?@[*]==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, descendants",
+ "selector": "$[?@..a==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, combined",
+ "selector": "$[?@.a[*].a==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, nested",
+ "selector": "$[?@[?@>1]]",
+ "document": [
+ [
+ 0
+ ],
+ [
+ 0,
+ 1
+ ],
+ [
+ 0,
+ 1,
+ 2
+ ],
+ [
+ 42
+ ]
+ ],
+ "result": [
+ [
+ 0,
+ 1,
+ 2
+ ],
+ [
+ 42
+ ]
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, name segment on primitive, selects nothing",
+ "selector": "$[?@.a == 1]",
+ "document": {
+ "a": 1
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, name segment on array, selects nothing",
+ "selector": "$[?@['0'] == 5]",
+ "document": [
+ [
+ 5,
+ 6
+ ]
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, index segment on object, selects nothing",
+ "selector": "$[?@[0] == 5]",
+ "document": [
+ {
+ "0": 5
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, followed by name selector",
+ "selector": "$[?@.a==1].b.x",
+ "document": [
+ {
+ "a": 1,
+ "b": {
+ "x": 2
+ }
+ }
+ ],
+ "result": [
+ 2
+ ],
+ "result_paths": [
+ "$[0]['b']['x']"
+ ]
+ },
+ {
+ "name": "filter, followed by child segment that selects multiple elements",
+ "selector": "$[?@.z=='_']['x','y']",
+ "document": [
+ {
+ "x": 1,
+ "y": null,
+ "z": "_"
+ }
+ ],
+ "result": [
+ 1,
+ null
+ ],
+ "result_paths": [
+ "$[0]['x']",
+ "$[0]['y']"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, equal",
+ "selector": "$[?(@[0, 0]==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, not equal",
+ "selector": "$[?(@[0, 0]!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, less-or-equal",
+ "selector": "$[?(@[0, 0]<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, equal",
+ "selector": "$[?(@['a', 'a']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, not equal",
+ "selector": "$[?(@['a', 'a']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, less-or-equal",
+ "selector": "$[?(@['a', 'a']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, equal",
+ "selector": "$[?(@[0, '0']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, not equal",
+ "selector": "$[?(@[0, '0']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, less-or-equal",
+ "selector": "$[?(@[0, '0']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, equal",
+ "selector": "$[?(@.*==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, not equal",
+ "selector": "$[?(@.*!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, less-or-equal",
+ "selector": "$[?(@.*<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, equal",
+ "selector": "$[?(@[0:0]==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, not equal",
+ "selector": "$[?(@[0:0]!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, less-or-equal",
+ "selector": "$[?(@[0:0]<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, index, equal",
+ "selector": "$[?($[0, 0]==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, index, not equal",
+ "selector": "$[?($[0, 0]!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, index, less-or-equal",
+ "selector": "$[?($[0, 0]<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, equal",
+ "selector": "$[?($['a', 'a']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, not equal",
+ "selector": "$[?($['a', 'a']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, less-or-equal",
+ "selector": "$[?($['a', 'a']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, equal",
+ "selector": "$[?($[0, '0']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, not equal",
+ "selector": "$[?($[0, '0']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, less-or-equal",
+ "selector": "$[?($[0, '0']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, equal",
+ "selector": "$[?($.*==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, not equal",
+ "selector": "$[?($.*!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, less-or-equal",
+ "selector": "$[?($.*<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, equal",
+ "selector": "$[?($[0:0]==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, not equal",
+ "selector": "$[?($[0:0]!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, less-or-equal",
+ "selector": "$[?($[0:0]<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, multiple selectors",
+ "selector": "$[?@.a,?@.b]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, comparison",
+ "selector": "$[?@.a=='b',?@.b=='x']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, overlapping",
+ "selector": "$[?@.a,?@.d]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and index",
+ "selector": "$[?@.a,1]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and wildcard",
+ "selector": "$[?@.a,*]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and slice",
+ "selector": "$[?@.a,1:]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "g": "h"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "g": "h"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, comparison filter, index and slice",
+ "selector": "$[1, ?@.a=='b', 1:]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, equals number, zero and negative zero",
+ "selector": "$[?@.a==0]",
+ "document": [
+ {
+ "a": 0,
+ "d": "e"
+ },
+ {
+ "a": 0.1,
+ "d": "f"
+ },
+ {
+ "a": "0",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, negative zero and zero",
+ "selector": "$[?@.a==-0]",
+ "document": [
+ {
+ "a": 0,
+ "d": "e"
+ },
+ {
+ "a": 0.1,
+ "d": "f"
+ },
+ {
+ "a": "0",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, with and without decimal fraction",
+ "selector": "$[?@.a==1.0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent",
+ "selector": "$[?@.a==1e2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent upper e",
+ "selector": "$[?@.a==1E2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, positive exponent",
+ "selector": "$[?@.a==1e+2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, negative exponent",
+ "selector": "$[?@.a==1e-2]",
+ "document": [
+ {
+ "a": 0.01,
+ "d": "e"
+ },
+ {
+ "a": 0.02,
+ "d": "f"
+ },
+ {
+ "a": "0.01",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.01,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent 0",
+ "selector": "$[?@.a==1e0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent -0",
+ "selector": "$[?@.a==1e-0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent +0",
+ "selector": "$[?@.a==1e+0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent leading -0",
+ "selector": "$[?@.a==1e-02]",
+ "document": [
+ {
+ "a": 0.01,
+ "d": "e"
+ },
+ {
+ "a": 0.02,
+ "d": "f"
+ },
+ {
+ "a": "0.01",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.01,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent +00",
+ "selector": "$[?@.a==1e+00]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction",
+ "selector": "$[?@.a==1.1]",
+ "document": [
+ {
+ "a": 1.1,
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ },
+ {
+ "a": "1.1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1.1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, trailing 0",
+ "selector": "$[?@.a==1.10]",
+ "document": [
+ {
+ "a": 1.1,
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ },
+ {
+ "a": "1.1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1.1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, exponent",
+ "selector": "$[?@.a==1.1e2]",
+ "document": [
+ {
+ "a": 110,
+ "d": "e"
+ },
+ {
+ "a": 110.1,
+ "d": "f"
+ },
+ {
+ "a": "110",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 110,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, positive exponent",
+ "selector": "$[?@.a==1.1e+2]",
+ "document": [
+ {
+ "a": 110,
+ "d": "e"
+ },
+ {
+ "a": 110.1,
+ "d": "f"
+ },
+ {
+ "a": "110",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 110,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, negative exponent",
+ "selector": "$[?@.a==1.1e-2]",
+ "document": [
+ {
+ "a": 0.011,
+ "d": "e"
+ },
+ {
+ "a": 0.012,
+ "d": "f"
+ },
+ {
+ "a": "0.011",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.011,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, invalid plus",
+ "selector": "$[?@.a==+1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid minus space",
+ "selector": "$[?@.a==- 1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid double minus",
+ "selector": "$[?@.a==--1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no int digit",
+ "selector": "$[?@.a==.1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid minus no int digit",
+ "selector": "$[?@.a==-.1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid 00",
+ "selector": "$[?@.a==00]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid leading 0",
+ "selector": "$[?@.a==01]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no fractional digit",
+ "selector": "$[?@.a==1.]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid middle minus",
+ "selector": "$[?@.a==1.-1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no fractional digit e",
+ "selector": "$[?@.a==1.e1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no e digit",
+ "selector": "$[?@.a==1e]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no e digit minus",
+ "selector": "$[?@.a==1e-]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid double e",
+ "selector": "$[?@.a==1eE1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e digit double minus",
+ "selector": "$[?@.a==1e--1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e digit plus minus",
+ "selector": "$[?@.a==1e+-1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e decimal",
+ "selector": "$[?@.a==1e2.3]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid multi e",
+ "selector": "$[?@.a==1e2e3]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals, special nothing",
+ "selector": "$.values[?length(@.a) == value($..c)]",
+ "document": {
+ "c": "cd",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ]
+ },
+ "result": [
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ],
+ "result_paths": [
+ "$['values'][1]",
+ "$['values'][2]"
+ ],
+ "tags": [
+ "function"
+ ]
+ },
+ {
+ "name": "filter, equals, empty node list and empty node list",
+ "selector": "$[?@.a == @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, equals, empty node list and special nothing",
+ "selector": "$[?@.a == length(@.b)]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, object data",
+ "selector": "$[?@<3]",
+ "document": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "results": [
+ [
+ 1,
+ 2
+ ],
+ [
+ 2,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, and binds more tightly than or",
+ "selector": "$[?@.a || @.b && @.c]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[4]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, left to right evaluation",
+ "selector": "$[?@.a && @.b || @.c]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 1,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 1,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, group terms, left",
+ "selector": "$[?(@.a || @.b) && @.c]",
+ "document": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[6]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, group terms, right",
+ "selector": "$[?@.a && (@.b || @.c)]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 2
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[5]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, single quote in double quotes",
+ "selector": "$[?@ == \"quoted' literal\"]",
+ "document": [
+ "quoted' literal",
+ "a",
+ "quoted\\' literal"
+ ],
+ "result": [
+ "quoted' literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, string literal, double quote in single quotes",
+ "selector": "$[?@ == 'quoted\" literal']",
+ "document": [
+ "quoted\" literal",
+ "a",
+ "quoted\\\" literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted\" literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, escaped single quote in single quotes",
+ "selector": "$[?@ == 'quoted\\' literal']",
+ "document": [
+ "quoted' literal",
+ "a",
+ "quoted\\' literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted' literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, escaped double quote in double quotes",
+ "selector": "$[?@ == \"quoted\\\" literal\"]",
+ "document": [
+ "quoted\" literal",
+ "a",
+ "quoted\\\" literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted\" literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, literal true must be compared",
+ "selector": "$[?true]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal false must be compared",
+ "selector": "$[?false]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal string must be compared",
+ "selector": "$[?'abc']",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal int must be compared",
+ "selector": "$[?2]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal float must be compared",
+ "selector": "$[?2.2]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal null must be compared",
+ "selector": "$[?null]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, and, literals must be compared",
+ "selector": "$[?true && false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, literals must be compared",
+ "selector": "$[?true || false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, and, right hand literal must be compared",
+ "selector": "$[?true == false && false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, right hand literal must be compared",
+ "selector": "$[?true == false || false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, and, left hand literal must be compared",
+ "selector": "$[?false && true == false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, left hand literal must be compared",
+ "selector": "$[?false || true == false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, true, incorrectly capitalized",
+ "selector": "$[?@==True]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "filter, false, incorrectly capitalized",
+ "selector": "$[?@==False]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "filter, null, incorrectly capitalized",
+ "selector": "$[?@==Null]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "index selector, first element",
+ "selector": "$[0]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, second element",
+ "selector": "$[1]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "second"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, out of bound",
+ "selector": "$[2]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, min exact index",
+ "selector": "$[-9007199254740991]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, max exact index",
+ "selector": "$[9007199254740991]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, min exact index - 1",
+ "selector": "$[-9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, max exact index + 1",
+ "selector": "$[9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, overflowing index",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, not actually an index, overflowing index leads into general text",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168SomeRandomText]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, negative",
+ "selector": "$[-1]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "second"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, more negative",
+ "selector": "$[-2]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, negative out of bound",
+ "selector": "$[-3]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, on object",
+ "selector": "$[0]",
+ "document": {
+ "foo": 1
+ },
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, leading 0",
+ "selector": "$[01]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, decimal",
+ "selector": "$[1.0]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, plus",
+ "selector": "$[+1]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, minus space",
+ "selector": "$[- 1]",
+ "invalid_selector": true,
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "index selector, -0",
+ "selector": "$[-0]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, leading -0",
+ "selector": "$[-01]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "name selector, double quotes",
+ "selector": "$[\"a\"]",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, absent data",
+ "selector": "$[\"c\"]",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, double quotes, array data",
+ "selector": "$[\"a\"]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, name, double quotes, contains single quote",
+ "selector": "$[\"a'\"]",
+ "document": {
+ "a'": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a\\'']"
+ ]
+ },
+ {
+ "name": "name selector, name, double quotes, nested",
+ "selector": "$[\"a\"][\"b\"][\"c\"]",
+ "document": {
+ "a": {
+ "b": {
+ "c": "C"
+ }
+ }
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['a']['b']['c']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0000",
+ "selector": "$[\"\u0000\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0001",
+ "selector": "$[\"\u0001\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0002",
+ "selector": "$[\"\u0002\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0003",
+ "selector": "$[\"\u0003\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0004",
+ "selector": "$[\"\u0004\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0005",
+ "selector": "$[\"\u0005\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0006",
+ "selector": "$[\"\u0006\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0007",
+ "selector": "$[\"\u0007\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0008",
+ "selector": "$[\"\b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0009",
+ "selector": "$[\"\t\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000A",
+ "selector": "$[\"\n\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000B",
+ "selector": "$[\"\u000b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000C",
+ "selector": "$[\"\f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000D",
+ "selector": "$[\"\r\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000E",
+ "selector": "$[\"\u000e\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000F",
+ "selector": "$[\"\u000f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0010",
+ "selector": "$[\"\u0010\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0011",
+ "selector": "$[\"\u0011\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0012",
+ "selector": "$[\"\u0012\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0013",
+ "selector": "$[\"\u0013\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0014",
+ "selector": "$[\"\u0014\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0015",
+ "selector": "$[\"\u0015\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0016",
+ "selector": "$[\"\u0016\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0017",
+ "selector": "$[\"\u0017\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0018",
+ "selector": "$[\"\u0018\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0019",
+ "selector": "$[\"\u0019\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001A",
+ "selector": "$[\"\u001a\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001B",
+ "selector": "$[\"\u001b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001C",
+ "selector": "$[\"\u001c\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001D",
+ "selector": "$[\"\u001d\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001E",
+ "selector": "$[\"\u001e\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001F",
+ "selector": "$[\"\u001f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0020",
+ "selector": "$[\" \"]",
+ "document": {
+ " ": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$[' ']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+007F",
+ "selector": "$[\"\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, supplementary plane character",
+ "selector": "$[\"𝄞\"]",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped double quote",
+ "selector": "$[\"\\\"\"]",
+ "document": {
+ "\"": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\"']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped reverse solidus",
+ "selector": "$[\"\\\\\"]",
+ "document": {
+ "\\": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\\\']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped solidus",
+ "selector": "$[\"\\/\"]",
+ "document": {
+ "/": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['/']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped backspace",
+ "selector": "$[\"\\b\"]",
+ "document": {
+ "\b": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\b']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped form feed",
+ "selector": "$[\"\\f\"]",
+ "document": {
+ "\f": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\f']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped line feed",
+ "selector": "$[\"\\n\"]",
+ "document": {
+ "\n": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\n']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped carriage return",
+ "selector": "$[\"\\r\"]",
+ "document": {
+ "\r": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\r']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped tab",
+ "selector": "$[\"\\t\"]",
+ "document": {
+ "\t": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\t']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped ☺, upper case hex",
+ "selector": "$[\"\\u263A\"]",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped ☺, lower case hex",
+ "selector": "$[\"\\u263a\"]",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate pair 𝄞",
+ "selector": "$[\"\\uD834\\uDD1E\"]",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate pair 😀",
+ "selector": "$[\"\\uD83D\\uDE00\"]",
+ "document": {
+ "😀": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['😀']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, before high surrogates",
+ "selector": "$[\"\\uD7FF\\uD7FF\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, after low surrogates",
+ "selector": "$[\"\\uE000\\uE000\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, invalid escaped single quote",
+ "selector": "$[\"\\'\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, embedded double quote",
+ "selector": "$[\"\"\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, incomplete escape",
+ "selector": "$[\"\\\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, escape at end of line",
+ "selector": "$[\"\\\n\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, question mark escape",
+ "selector": "$[\"\\?\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, bell escape",
+ "selector": "$[\"\\a\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, vertical tab escape",
+ "selector": "$[\"\\v\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, 0 escape",
+ "selector": "$[\"\\0\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, x escape",
+ "selector": "$[\"\\x12\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, n escape",
+ "selector": "$[\"\\N{LATIN CAPITAL LETTER A}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape no hex",
+ "selector": "$[\"\\u\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape too few hex",
+ "selector": "$[\"\\u123\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape upper u",
+ "selector": "$[\"\\U1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape upper u long",
+ "selector": "$[\"\\U0010FFFF\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape plus",
+ "selector": "$[\"\\u+1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape brackets",
+ "selector": "$[\"\\u{1234}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape brackets long",
+ "selector": "$[\"\\u{10ffff}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, single high surrogate",
+ "selector": "$[\"\\uD800\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, single low surrogate",
+ "selector": "$[\"\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, high high surrogate",
+ "selector": "$[\"\\uD800\\uD800\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, low low surrogate",
+ "selector": "$[\"\\uDC00\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate non-surrogate",
+ "selector": "$[\"\\uD800\\u1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, non-surrogate surrogate",
+ "selector": "$[\"\\u1234\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate supplementary",
+ "selector": "$[\"\\uD800𝄞\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, supplementary surrogate",
+ "selector": "$[\"𝄞\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate incomplete low",
+ "selector": "$[\"\\uD800\\uDC0\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes",
+ "selector": "$['a']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, absent data",
+ "selector": "$['c']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, single quotes, array data",
+ "selector": "$['a']",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0000",
+ "selector": "$['\u0000']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0001",
+ "selector": "$['\u0001']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0002",
+ "selector": "$['\u0002']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0003",
+ "selector": "$['\u0003']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0004",
+ "selector": "$['\u0004']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0005",
+ "selector": "$['\u0005']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0006",
+ "selector": "$['\u0006']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0007",
+ "selector": "$['\u0007']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0008",
+ "selector": "$['\b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0009",
+ "selector": "$['\t']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000A",
+ "selector": "$['\n']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000B",
+ "selector": "$['\u000b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000C",
+ "selector": "$['\f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000D",
+ "selector": "$['\r']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000E",
+ "selector": "$['\u000e']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000F",
+ "selector": "$['\u000f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0010",
+ "selector": "$['\u0010']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0011",
+ "selector": "$['\u0011']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0012",
+ "selector": "$['\u0012']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0013",
+ "selector": "$['\u0013']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0014",
+ "selector": "$['\u0014']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0015",
+ "selector": "$['\u0015']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0016",
+ "selector": "$['\u0016']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0017",
+ "selector": "$['\u0017']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0018",
+ "selector": "$['\u0018']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0019",
+ "selector": "$['\u0019']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001A",
+ "selector": "$['\u001a']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001B",
+ "selector": "$['\u001b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001C",
+ "selector": "$['\u001c']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001D",
+ "selector": "$['\u001d']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001E",
+ "selector": "$['\u001e']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001F",
+ "selector": "$['\u001f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0020",
+ "selector": "$[' ']",
+ "document": {
+ " ": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$[' ']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped single quote",
+ "selector": "$['\\'']",
+ "document": {
+ "'": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\'']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped reverse solidus",
+ "selector": "$['\\\\']",
+ "document": {
+ "\\": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\\\']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped solidus",
+ "selector": "$['\\/']",
+ "document": {
+ "/": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['/']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped backspace",
+ "selector": "$['\\b']",
+ "document": {
+ "\b": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\b']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped form feed",
+ "selector": "$['\\f']",
+ "document": {
+ "\f": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\f']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped line feed",
+ "selector": "$['\\n']",
+ "document": {
+ "\n": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\n']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped carriage return",
+ "selector": "$['\\r']",
+ "document": {
+ "\r": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\r']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped tab",
+ "selector": "$['\\t']",
+ "document": {
+ "\t": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\t']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped ☺, upper case hex",
+ "selector": "$['\\u263A']",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped ☺, lower case hex",
+ "selector": "$['\\u263a']",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, surrogate pair 𝄞",
+ "selector": "$['\\uD834\\uDD1E']",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, surrogate pair 😀",
+ "selector": "$['\\uD83D\\uDE00']",
+ "document": {
+ "😀": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['😀']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, invalid escaped double quote",
+ "selector": "$['\\\"']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, single quotes, embedded single quote",
+ "selector": "$[''']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, single quotes, incomplete escape",
+ "selector": "$['\\']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, empty",
+ "selector": "$[\"\"]",
+ "document": {
+ "a": "A",
+ "b": "B",
+ "": "C"
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, empty",
+ "selector": "$['']",
+ "document": {
+ "a": "A",
+ "b": "B",
+ "": "C"
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['']"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector",
+ "selector": "$[1:3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with step",
+ "selector": "$[1:6:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 3,
+ 5
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted, short form",
+ "selector": "$[:]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted, long form",
+ "selector": "$[::]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with start omitted",
+ "selector": "$[:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with start and end omitted",
+ "selector": "$[::2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 8
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]",
+ "$[4]",
+ "$[6]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default start and end",
+ "selector": "$[::-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default start",
+ "selector": "$[:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default end",
+ "selector": "$[2::-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, larger negative step",
+ "selector": "$[::-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 1
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with default step",
+ "selector": "$[-1:-3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with negative step",
+ "selector": "$[-1:-3:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with larger negative step",
+ "selector": "$[-1:-6:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 7,
+ 5
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[7]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, larger negative range with larger negative step",
+ "selector": "$[-1:-7:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 7,
+ 5
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[7]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, positive to",
+ "selector": "$[-5:7]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 5,
+ 6
+ ],
+ "result_paths": [
+ "$[5]",
+ "$[6]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from",
+ "selector": "$[-2:]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, positive from, negative to",
+ "selector": "$[1:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, positive to, negative step",
+ "selector": "$[-1:1:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, positive from, negative to, negative step",
+ "selector": "$[7:-5:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 7,
+ 6
+ ],
+ "result_paths": [
+ "$[7]",
+ "$[6]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, in serial, on nested array",
+ "selector": "$[1:3][1:2]",
+ "document": [
+ [
+ "a",
+ "b",
+ "c"
+ ],
+ [
+ "d",
+ "e",
+ "f"
+ ],
+ [
+ "g",
+ "h",
+ "i"
+ ]
+ ],
+ "result": [
+ "e",
+ "h"
+ ],
+ "result_paths": [
+ "$[1][1]",
+ "$[2][1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, in serial, on flat array",
+ "selector": "$[1:3][::]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, negative to, positive step",
+ "selector": "$[-5:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 5,
+ 6,
+ 7
+ ],
+ "result_paths": [
+ "$[5]",
+ "$[6]",
+ "$[7]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, too many colons",
+ "selector": "$[1:2:3:4]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, non-integer array index",
+ "selector": "$[1:2:a]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, zero step",
+ "selector": "$[1:2:0]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, empty range",
+ "selector": "$[2:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted with empty array",
+ "selector": "$[:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with empty array",
+ "selector": "$[::-1]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, maximal range with positive step",
+ "selector": "$[0:10]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, maximal range with negative step",
+ "selector": "$[9:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large to value",
+ "selector": "$[2:113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small from value",
+ "selector": "$[-113667776004:1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large from value with negative step",
+ "selector": "$[113667776004:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small to value with negative step",
+ "selector": "$[3:-113667776004:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 3,
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large step",
+ "selector": "$[1:10:113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small step",
+ "selector": "$[-1:-10:-113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9
+ ],
+ "result_paths": [
+ "$[9]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, min exact",
+ "selector": "$[-9007199254740991::]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, max exact",
+ "selector": "$[9007199254740991::]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, min exact - 1",
+ "selector": "$[-9007199254740992::]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, max exact + 1",
+ "selector": "$[9007199254740992::]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, min exact",
+ "selector": "$[:-9007199254740991:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, max exact",
+ "selector": "$[:9007199254740991:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, min exact - 1",
+ "selector": "$[:-9007199254740992:]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, max exact + 1",
+ "selector": "$[:9007199254740992:]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, min exact",
+ "selector": "$[::-9007199254740991]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, max exact",
+ "selector": "$[::9007199254740991]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, min exact - 1",
+ "selector": "$[::-9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, max exact + 1",
+ "selector": "$[::9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing to value",
+ "selector": "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing from value",
+ "selector": "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing from value with negative step",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing to value with negative step",
+ "selector": "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing step",
+ "selector": "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing step",
+ "selector": "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, leading 0",
+ "selector": "$[01::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, decimal",
+ "selector": "$[1.0::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, plus",
+ "selector": "$[+1::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, minus space",
+ "selector": "$[- 1::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, -0",
+ "selector": "$[-0::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, leading -0",
+ "selector": "$[-01::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, leading 0",
+ "selector": "$[:01:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, decimal",
+ "selector": "$[:1.0:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, plus",
+ "selector": "$[:+1:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, minus space",
+ "selector": "$[:- 1:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, -0",
+ "selector": "$[:-0:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, leading -0",
+ "selector": "$[:-01:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, leading 0",
+ "selector": "$[::01]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, decimal",
+ "selector": "$[::1.0]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, plus",
+ "selector": "$[::+1]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, minus space",
+ "selector": "$[::- 1]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, -0",
+ "selector": "$[::-0]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, leading -0",
+ "selector": "$[::-01]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "functions, count, count function",
+ "selector": "$[?count(@..*)>2]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, single-node arg",
+ "selector": "$[?count(@.a)>1]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, multiple-selector arg",
+ "selector": "$[?count(@['a','d'])>1]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, number",
+ "selector": "$[?count(1)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, string",
+ "selector": "$[?count('string')>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, true",
+ "selector": "$[?count(true)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, false",
+ "selector": "$[?count(false)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, null",
+ "selector": "$[?count(null)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, result must be compared",
+ "selector": "$[?count(@..*)]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, no params",
+ "selector": "$[?count()==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, too many params",
+ "selector": "$[?count(@.a,@.b)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, length, string data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "d"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, string data, unicode",
+ "selector": "$[?length(@)==2]",
+ "document": [
+ "☺",
+ "☺☺",
+ "☺☺☺",
+ "ж",
+ "жж",
+ "жжж",
+ "磨",
+ "阿美",
+ "形声字"
+ ],
+ "result": [
+ "☺☺",
+ "жж",
+ "阿美"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[4]",
+ "$[7]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, array data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ]
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, missing data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, number arg",
+ "selector": "$[?length(1)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, true arg",
+ "selector": "$[?length(true)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, false arg",
+ "selector": "$[?length(false)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, null arg",
+ "selector": "$[?length(null)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, result must be compared",
+ "selector": "$[?length(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, no params",
+ "selector": "$[?length()==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, too many params",
+ "selector": "$[?length(@.a,@.b)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, non-singular query arg",
+ "selector": "$[?length(@.*)<3]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, arg is a function expression",
+ "selector": "$.values[?length(@.a)==length(value($..c))]",
+ "document": {
+ "c": "cd",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "d"
+ }
+ ]
+ },
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$['values'][0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, arg is special nothing",
+ "selector": "$[?length(value(@.a))>0]",
+ "document": [
+ {
+ "a": "ab"
+ },
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, match, found match",
+ "selector": "$[?match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, double quotes",
+ "selector": "$[?match(@.a, \"a.*\")]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, regex from the document",
+ "selector": "$.values[?match(@, $.regex)]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab"
+ ],
+ "result_paths": [
+ "$['values'][2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, don't select match",
+ "selector": "$[?!match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, not a match",
+ "selector": "$[?match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, select non-match",
+ "selector": "$[?!match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, non-string first arg",
+ "selector": "$[?match(1, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, non-string second arg",
+ "selector": "$[?match(@.a, 1)]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode char class, uppercase",
+ "selector": "$[?match(@, '\\\\p{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ "жЖ",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "Ж"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode char class negated, uppercase",
+ "selector": "$[?match(@, '\\\\P{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "ж",
+ "1"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode, surrogate pair",
+ "selector": "$[?match(@, 'a.b')]",
+ "document": [
+ "a𐄁b",
+ "ab",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "a𐄁b"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot matcher on \\u2028",
+ "selector": "$[?match(@, '.')]",
+ "document": [
+ "
",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot matcher on \\u2029",
+ "selector": "$[?match(@, '.')]",
+ "document": [
+ "
",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, result cannot be compared",
+ "selector": "$[?match(@.a, 'a.*')==true]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, too few params",
+ "selector": "$[?match(@.a)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, too many params",
+ "selector": "$[?match(@.a,@.b,@.c)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, arg is a function expression",
+ "selector": "$.values[?match(@.a, value($..['regex']))]",
+ "document": {
+ "regex": "a.*",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "ba"
+ }
+ ]
+ },
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$['values'][0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot in character class",
+ "selector": "$[?match(@, 'a[.b]c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc"
+ ],
+ "result": [
+ "abc",
+ "a.c"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped dot",
+ "selector": "$[?match(@, 'a\\\\.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc"
+ ],
+ "result": [
+ "a.c"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped backslash before dot",
+ "selector": "$[?match(@, 'a\\\\\\\\.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc",
+ "a\\
c"
+ ],
+ "result": [
+ "a\\
c"
+ ],
+ "result_paths": [
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped left square bracket",
+ "selector": "$[?match(@, 'a\\\\[.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "a[
c"
+ ],
+ "result": [
+ "a[
c"
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped right square bracket",
+ "selector": "$[?match(@, 'a[\\\\].]c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "a
c",
+ "a]c"
+ ],
+ "result": [
+ "a.c",
+ "a]c"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, explicit caret",
+ "selector": "$[?match(@, '^ab.*')]",
+ "document": [
+ "abc",
+ "axc",
+ "ab",
+ "xab"
+ ],
+ "result": [
+ "abc",
+ "ab"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, explicit dollar",
+ "selector": "$[?match(@, '.*bc$')]",
+ "document": [
+ "abc",
+ "axc",
+ "ab",
+ "abcx"
+ ],
+ "result": [
+ "abc"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, search, at the end",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, double quotes",
+ "selector": "$[?search(@.a, \"a.*\")]",
+ "document": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, at the start",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab is at the start"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab is at the start"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, in the middle",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, regex from the document",
+ "selector": "$.values[?search(@, $.regex)]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab",
+ "bba",
+ "bbab"
+ ],
+ "result_paths": [
+ "$['values'][2]",
+ "$['values'][3]",
+ "$['values'][4]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, don't select match",
+ "selector": "$[?!search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, not a match",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, select non-match",
+ "selector": "$[?!search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, non-string first arg",
+ "selector": "$[?search(1, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, non-string second arg",
+ "selector": "$[?search(@.a, 1)]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode char class, uppercase",
+ "selector": "$[?search(@, '\\\\p{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ "жЖ",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "Ж",
+ "жЖ"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode char class negated, uppercase",
+ "selector": "$[?search(@, '\\\\P{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "ж",
+ "1"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode, surrogate pair",
+ "selector": "$[?search(@, 'a.b')]",
+ "document": [
+ "a𐄁bc",
+ "abc",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "a𐄁bc"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot matcher on \\u2028",
+ "selector": "$[?search(@, '.')]",
+ "document": [
+ "
",
+ "\r
\n",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
",
+ "\r
\n"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot matcher on \\u2029",
+ "selector": "$[?search(@, '.')]",
+ "document": [
+ "
",
+ "\r
\n",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
",
+ "\r
\n"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, result cannot be compared",
+ "selector": "$[?search(@.a, 'a.*')==true]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, too few params",
+ "selector": "$[?search(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, too many params",
+ "selector": "$[?search(@.a,@.b,@.c)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, arg is a function expression",
+ "selector": "$.values[?search(@, value($..['regex']))]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab",
+ "bba",
+ "bbab"
+ ],
+ "result_paths": [
+ "$['values'][2]",
+ "$['values'][3]",
+ "$['values'][4]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot in character class",
+ "selector": "$[?search(@, 'a[.b]c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y"
+ ],
+ "result": [
+ "x abc y",
+ "x a.c y"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped dot",
+ "selector": "$[?search(@, 'a\\\\.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y"
+ ],
+ "result": [
+ "x a.c y"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped backslash before dot",
+ "selector": "$[?search(@, 'a\\\\\\\\.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y",
+ "x a\\
c y"
+ ],
+ "result": [
+ "x a\\
c y"
+ ],
+ "result_paths": [
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped left square bracket",
+ "selector": "$[?search(@, 'a\\\\[.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x a[
c y"
+ ],
+ "result": [
+ "x a[
c y"
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped right square bracket",
+ "selector": "$[?search(@, 'a[\\\\].]c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x a
c y",
+ "x a]c y"
+ ],
+ "result": [
+ "x a.c y",
+ "x a]c y"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, value, single-value nodelist",
+ "selector": "$[?value(@.*)==4]",
+ "document": [
+ [
+ 4
+ ],
+ {
+ "foo": 4
+ },
+ [
+ 5
+ ],
+ {
+ "foo": 5
+ },
+ 4
+ ],
+ "result": [
+ [
+ 4
+ ],
+ {
+ "foo": 4
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, multi-value nodelist",
+ "selector": "$[?value(@.*)==4]",
+ "document": [
+ [
+ 4,
+ 4
+ ],
+ {
+ "foo": 4,
+ "bar": 4
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, too few params",
+ "selector": "$[?value()==4]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, too many params",
+ "selector": "$[?value(@.a,@.b)==4]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, result must be compared",
+ "selector": "$[?value(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between question mark and expression",
+ "selector": "$[? @.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between question mark and expression",
+ "selector": "$[?\n@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between question mark and expression",
+ "selector": "$[?\t@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between question mark and expression",
+ "selector": "$[?\r@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between question mark and parenthesized expression",
+ "selector": "$[? (@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between question mark and parenthesized expression",
+ "selector": "$[?\n(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between question mark and parenthesized expression",
+ "selector": "$[?\t(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between question mark and parenthesized expression",
+ "selector": "$[?\r(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between parenthesized expression and bracket",
+ "selector": "$[?(@.a) ]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\n]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\t]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\r]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between bracket and question mark",
+ "selector": "$[ ?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between bracket and question mark",
+ "selector": "$[\n?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between bracket and question mark",
+ "selector": "$[\t?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between bracket and question mark",
+ "selector": "$[\r?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between function name and parenthesis",
+ "selector": "$[?count (@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between function name and parenthesis",
+ "selector": "$[?count\n(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between function name and parenthesis",
+ "selector": "$[?count\t(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between function name and parenthesis",
+ "selector": "$[?count\r(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between parenthesis and arg",
+ "selector": "$[?count( @.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between parenthesis and arg",
+ "selector": "$[?count(\n@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between parenthesis and arg",
+ "selector": "$[?count(\t@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between parenthesis and arg",
+ "selector": "$[?count(\r@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between arg and comma",
+ "selector": "$[?search(@ ,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between arg and comma",
+ "selector": "$[?search(@\n,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between arg and comma",
+ "selector": "$[?search(@\t,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between arg and comma",
+ "selector": "$[?search(@\r,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between comma and arg",
+ "selector": "$[?search(@, '[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between comma and arg",
+ "selector": "$[?search(@,\n'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between comma and arg",
+ "selector": "$[?search(@,\t'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between comma and arg",
+ "selector": "$[?search(@,\r'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between arg and parenthesis",
+ "selector": "$[?count(@.* )==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between arg and parenthesis",
+ "selector": "$[?count(@.*\n)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between arg and parenthesis",
+ "selector": "$[?count(@.*\t)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between arg and parenthesis",
+ "selector": "$[?count(@.*\r)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, spaces in a relative singular selector",
+ "selector": "$[?length(@ .a .b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newlines in a relative singular selector",
+ "selector": "$[?length(@\n.a\n.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tabs in a relative singular selector",
+ "selector": "$[?length(@\t.a\t.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, returns in a relative singular selector",
+ "selector": "$[?length(@\r.a\r.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, spaces in an absolute singular selector",
+ "selector": "$..[?length(@)==length($ [0] .a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newlines in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\n[0]\n.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tabs in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\t[0]\t.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, returns in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\r[0]\r.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before ||",
+ "selector": "$[?@.a ||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before ||",
+ "selector": "$[?@.a\n||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before ||",
+ "selector": "$[?@.a\t||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before ||",
+ "selector": "$[?@.a\r||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after ||",
+ "selector": "$[?@.a|| @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after ||",
+ "selector": "$[?@.a||\n@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after ||",
+ "selector": "$[?@.a||\t@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after ||",
+ "selector": "$[?@.a||\r@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before &&",
+ "selector": "$[?@.a &&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before &&",
+ "selector": "$[?@.a\n&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before &&",
+ "selector": "$[?@.a\t&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before &&",
+ "selector": "$[?@.a\r&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before ==",
+ "selector": "$[?@.a ==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before ==",
+ "selector": "$[?@.a\n==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before ==",
+ "selector": "$[?@.a\t==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before ==",
+ "selector": "$[?@.a\r==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after ==",
+ "selector": "$[?@.a== @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after ==",
+ "selector": "$[?@.a==\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after ==",
+ "selector": "$[?@.a==\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after ==",
+ "selector": "$[?@.a==\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before !=",
+ "selector": "$[?@.a !=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before !=",
+ "selector": "$[?@.a\n!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before !=",
+ "selector": "$[?@.a\t!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before !=",
+ "selector": "$[?@.a\r!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after !=",
+ "selector": "$[?@.a!= @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after !=",
+ "selector": "$[?@.a!=\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after !=",
+ "selector": "$[?@.a!=\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after !=",
+ "selector": "$[?@.a!=\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before <",
+ "selector": "$[?@.a <@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before <",
+ "selector": "$[?@.a\n<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before <",
+ "selector": "$[?@.a\t<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before <",
+ "selector": "$[?@.a\r<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after <",
+ "selector": "$[?@.a< @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after <",
+ "selector": "$[?@.a<\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after <",
+ "selector": "$[?@.a<\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after <",
+ "selector": "$[?@.a<\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before >",
+ "selector": "$[?@.b >@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before >",
+ "selector": "$[?@.b\n>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before >",
+ "selector": "$[?@.b\t>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before >",
+ "selector": "$[?@.b\r>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after >",
+ "selector": "$[?@.b> @.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after >",
+ "selector": "$[?@.b>\n@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after >",
+ "selector": "$[?@.b>\t@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after >",
+ "selector": "$[?@.b>\r@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before <=",
+ "selector": "$[?@.a <=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before <=",
+ "selector": "$[?@.a\n<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before <=",
+ "selector": "$[?@.a\t<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before <=",
+ "selector": "$[?@.a\r<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after <=",
+ "selector": "$[?@.a<= @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after <=",
+ "selector": "$[?@.a<=\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after <=",
+ "selector": "$[?@.a<=\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after <=",
+ "selector": "$[?@.a<=\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before >=",
+ "selector": "$[?@.b >=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before >=",
+ "selector": "$[?@.b\n>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before >=",
+ "selector": "$[?@.b\t>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before >=",
+ "selector": "$[?@.b\r>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after >=",
+ "selector": "$[?@.b>= @.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after >=",
+ "selector": "$[?@.b>=\n@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after >=",
+ "selector": "$[?@.b>=\t@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after >=",
+ "selector": "$[?@.b>=\r@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space between logical not and test expression",
+ "selector": "$[?! @.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline between logical not and test expression",
+ "selector": "$[?!\n@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab between logical not and test expression",
+ "selector": "$[?!\t@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return between logical not and test expression",
+ "selector": "$[?!\r@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space between logical not and parenthesized expression",
+ "selector": "$[?! (@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline between logical not and parenthesized expression",
+ "selector": "$[?!\n(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab between logical not and parenthesized expression",
+ "selector": "$[?!\t(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return between logical not and parenthesized expression",
+ "selector": "$[?!\r(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between root and bracket",
+ "selector": "$ ['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between root and bracket",
+ "selector": "$\n['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between root and bracket",
+ "selector": "$\t['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between root and bracket",
+ "selector": "$\r['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between bracket and bracket",
+ "selector": "$['a'] ['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between bracket and bracket",
+ "selector": "$['a'] \n['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between bracket and bracket",
+ "selector": "$['a'] \t['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between bracket and bracket",
+ "selector": "$['a'] \r['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between root and dot",
+ "selector": "$ .a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between root and dot",
+ "selector": "$\n.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between root and dot",
+ "selector": "$\t.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between root and dot",
+ "selector": "$\r.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between dot and name",
+ "selector": "$. a",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between dot and name",
+ "selector": "$.\na",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between dot and name",
+ "selector": "$.\ta",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between dot and name",
+ "selector": "$.\ra",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between recursive descent and name",
+ "selector": "$.. a",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between recursive descent and name",
+ "selector": "$..\na",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between recursive descent and name",
+ "selector": "$..\ta",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between recursive descent and name",
+ "selector": "$..\ra",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between bracket and selector",
+ "selector": "$[ 'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between bracket and selector",
+ "selector": "$[\n'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between bracket and selector",
+ "selector": "$[\t'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between bracket and selector",
+ "selector": "$[\r'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between selector and bracket",
+ "selector": "$['a' ]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between selector and bracket",
+ "selector": "$['a'\n]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between selector and bracket",
+ "selector": "$['a'\t]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between selector and bracket",
+ "selector": "$['a'\r]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between selector and comma",
+ "selector": "$['a' ,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between selector and comma",
+ "selector": "$['a'\n,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between selector and comma",
+ "selector": "$['a'\t,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between selector and comma",
+ "selector": "$['a'\r,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between comma and selector",
+ "selector": "$['a', 'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between comma and selector",
+ "selector": "$['a',\n'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between comma and selector",
+ "selector": "$['a',\t'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between comma and selector",
+ "selector": "$['a',\r'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between start and colon",
+ "selector": "$[1 :5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between start and colon",
+ "selector": "$[1\n:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between start and colon",
+ "selector": "$[1\t:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between start and colon",
+ "selector": "$[1\r:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between colon and end",
+ "selector": "$[1: 5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between colon and end",
+ "selector": "$[1:\n5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between colon and end",
+ "selector": "$[1:\t5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between colon and end",
+ "selector": "$[1:\r5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between end and colon",
+ "selector": "$[1:5 :2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between end and colon",
+ "selector": "$[1:5\n:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between end and colon",
+ "selector": "$[1:5\t:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between end and colon",
+ "selector": "$[1:5\r:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between colon and step",
+ "selector": "$[1:5: 2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between colon and step",
+ "selector": "$[1:5:\n2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between colon and step",
+ "selector": "$[1:5:\t2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between colon and step",
+ "selector": "$[1:5:\r2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ }
+ ]
+}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
index 6871a56511890..1d1eb4be3b431 100644
--- a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
@@ -49,6 +49,19 @@ public function testAllAuthors()
], $result);
}
+ public function testAllAuthorsWithBrackets()
+ {
+ $result = self::getBookstoreCrawler()->find('$..["author"]');
+
+ $this->assertCount(4, $result);
+ $this->assertSame([
+ 'Nigel Rees',
+ 'Evelyn Waugh',
+ 'Herman Melville',
+ 'J. R. R. Tolkien',
+ ], $result);
+ }
+
public function testAllThingsInStore()
{
$result = self::getBookstoreCrawler()->find('$.store.*');
@@ -58,6 +71,15 @@ public function testAllThingsInStore()
$this->assertArrayHasKey('color', $result[1]);
}
+ public function testAllThingsInStoreWithBrackets()
+ {
+ $result = self::getBookstoreCrawler()->find('$["store"][*]');
+
+ $this->assertCount(2, $result);
+ $this->assertCount(4, $result[0]);
+ $this->assertArrayHasKey('color', $result[1]);
+ }
+
public function testEscapedDoubleQuotesInFieldName()
{
$crawler = new JsonCrawler(<<assertSame(42, $result[0]);
}
+ public function testMultipleKeysAtOnce()
+ {
+ $crawler = new JsonCrawler(<<find("$['a', 'b', 3]");
+
+ $this->assertSame([
+ ['b"c' => 42],
+ ['c' => 43],
+ ], $result);
+ }
+
+ public function testMultipleKeysAtOnceOnArray()
+ {
+ $crawler = new JsonCrawler(<<find("$[0, 2, 'a,b,c', -1]");
+
+ $this->assertCount(4, $result);
+ $this->assertSame(['a' => 1], $result[0]);
+ $this->assertSame(['c' => 3], $result[1]);
+ $this->assertSame(['a,b,c' => 5], $result[2]);
+ $this->assertSame(['d' => 4], $result[3]);
+ }
+
public function testBasicNameSelector()
{
$result = self::getBookstoreCrawler()->find('$.store.book')[0];
@@ -77,6 +128,14 @@ public function testBasicNameSelector()
$this->assertSame('Nigel Rees', $result[0]['author']);
}
+ public function testBasicNameSelectorWithBrackts()
+ {
+ $result = self::getBookstoreCrawler()->find('$["store"]["book"]')[0];
+
+ $this->assertCount(4, $result);
+ $this->assertSame('Nigel Rees', $result[0]['author']);
+ }
+
public function testAllPrices()
{
$result = self::getBookstoreCrawler()->find('$.store..price');
@@ -121,6 +180,25 @@ public function testBooksWithIsbn()
], [$result[0]['isbn'], $result[1]['isbn']]);
}
+ public function testBooksWithPublisherAddress()
+ {
+ $result = self::getBookstoreCrawler()->find('$..book[?(@.publisher.address)]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testBooksWithBracketsAndFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$..["book"][?(@.isbn)]');
+
+ $this->assertCount(2, $result);
+ $this->assertSame([
+ '0-553-21311-3',
+ '0-395-19395-8',
+ ], [$result[0]['isbn'], $result[1]['isbn']]);
+ }
+
public function testBooksLessThanTenDollars()
{
$result = self::getBookstoreCrawler()->find('$..book[?(@.price < 10)]');
@@ -216,6 +294,14 @@ public function testEverySecondElementReverseSlice()
$this->assertSame([6, 2, 5], $result);
}
+ public function testEverySecondElementReverseSliceAndBrackets()
+ {
+ $crawler = self::getSimpleCollectionCrawler();
+
+ $result = $crawler->find('$["a"][::-2]');
+ $this->assertSame([6, 2, 5], $result);
+ }
+
public function testEmptyResults()
{
$crawler = self::getSimpleCollectionCrawler();
@@ -344,6 +430,50 @@ public function testValueFunction()
$this->assertSame('Sayings of the Century', $result[0]['title']);
}
+ public function testDeepExpressionInFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.address.city == "Springfield")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testWildcardInFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.* == "my-publisher")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testWildcardInFunction()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?match(@.publisher.*.city, "Spring.+")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testUseAtSymbolReturnsAll()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@ == @)]');
+
+ $this->assertSame([
+ 'red',
+ 399,
+ ], $result);
+ }
+
+ public function testUseAtSymbolAloneReturnsAll()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@)]');
+
+ $this->assertSame([
+ 'red',
+ 399,
+ ], $result);
+ }
+
public function testValueFunctionWithOuterParentheses()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?(value(@.price) == 8.95)]');
@@ -370,6 +500,28 @@ public function testLengthFunctionWithOuterParentheses()
$this->assertSame('J. R. R. Tolkien', $result[1]['author']);
}
+ public function testMatchFunctionWithMultipleSpacesTrimmed()
+ {
+ $result = self::getBookstoreCrawler()->find("$.store.book[?(match(@.title, 'Sword of Honour'))]");
+
+ $this->assertSame([], $result);
+ }
+
+ public function testFilterMultiline()
+ {
+ $result = self::getBookstoreCrawler()->find(
+ '$
+ .store
+ .book[?
+ length(@.author)>12
+ ]'
+ );
+
+ $this->assertCount(2, $result);
+ $this->assertSame('Herman Melville', $result[0]['author']);
+ $this->assertSame('J. R. R. Tolkien', $result[1]['author']);
+ }
+
public function testCountFunction()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?count(@.extra) != 0]');
@@ -404,6 +556,260 @@ public function testAcceptsJsonPath()
$this->assertSame('red', $result[0]['color']);
}
+ public function testStarAsKey()
+ {
+ $crawler = new JsonCrawler(<<find('$["*"]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame(['a' => 1, 'b' => 2], $result[0]);
+ }
+
+ /**
+ * @dataProvider provideUnicodeEscapeSequencesProvider
+ */
+ public function testUnicodeEscapeSequences(string $jsonPath, array $expected)
+ {
+ $this->assertSame($expected, self::getUnicodeDocumentCrawler()->find($jsonPath));
+ }
+
+ public static function provideUnicodeEscapeSequencesProvider(): array
+ {
+ return [
+ [
+ '$["caf\u00e9"]',
+ ['coffee'],
+ ],
+ [
+ '$["\u65e5\u672c"]',
+ ['Japan'],
+ ],
+ [
+ '$["M\u00fcller"]',
+ [],
+ ],
+ [
+ '$["emoji\ud83d\ude00"]',
+ ['smiley'],
+ ],
+ [
+ '$["tab\there"]',
+ ['with tab'],
+ ],
+ [
+ '$["quote\"here"]',
+ ['with quote'],
+ ],
+ [
+ '$["backslash\\\\here"]',
+ ['with backslash'],
+ ],
+ [
+ '$["apostrophe\'here"]',
+ ['with apostrophe'],
+ ],
+ [
+ '$["control\u0001char"]',
+ ['with control char'],
+ ],
+ [
+ '$["\u0063af\u00e9"]',
+ ['coffee'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSingleQuotedStringProvider
+ */
+ public function testSingleQuotedStrings(string $jsonPath, array $expected)
+ {
+ $this->assertSame($expected, self::getUnicodeDocumentCrawler()->find($jsonPath));
+ }
+
+ public static function provideSingleQuotedStringProvider(): array
+ {
+ return [
+ [
+ "$['caf\\u00e9']",
+ ['coffee'],
+ ],
+ [
+ "$['\\u65e5\\u672c']",
+ ['Japan'],
+ ],
+ [
+ "$['quote\"here']",
+ ['with quote'],
+ ],
+ [
+ "$['M\\u00fcller']",
+ [],
+ ],
+ [
+ "$['emoji\\ud83d\\ude00']",
+ ['smiley'],
+ ],
+ [
+ "$['tab\\there']",
+ ['with tab'],
+ ],
+ [
+ "$['quote\\\"here']",
+ ['with quote'],
+ ],
+ [
+ "$['backslash\\\\here']",
+ ['with backslash'],
+ ],
+ [
+ "$['apostrophe\\'here']",
+ ['with apostrophe'],
+ ],
+ [
+ "$['control\\u0001char']",
+ ['with control char'],
+ ],
+ [
+ "$['\\u0063af\\u00e9']",
+ ['coffee'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideFilterWithUnicodeProvider
+ */
+ public function testFilterWithUnicodeStrings(string $jsonPath, int $expectedCount, string $expectedCountry)
+ {
+ $result = self::getUnicodeDocumentCrawler()->find($jsonPath);
+
+ $this->assertCount($expectedCount, $result);
+
+ if ($expectedCount > 0) {
+ $this->assertSame($expectedCountry, $result[0]['country']);
+ }
+ }
+
+ public static function provideFilterWithUnicodeProvider(): array
+ {
+ return [
+ [
+ '$.users[?(@.name == "caf\u00e9")]',
+ 1,
+ 'France',
+ ],
+ [
+ '$.users[?(@.name == "\u65e5\u672c\u592a\u90ce")]',
+ 1,
+ 'Japan',
+ ],
+ [
+ '$.users[?(@.name == "Jos\u00e9")]',
+ 1,
+ 'Spain',
+ ],
+ [
+ '$.users[?(@.name == "John")]',
+ 1,
+ 'USA',
+ ],
+ [
+ '$.users[?(@.name == "NonExistent\u0020Name")]',
+ 0,
+ '',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInvalidUnicodeSequenceProvider
+ */
+ public function testInvalidUnicodeSequencesAreProcessedAsLiterals(string $jsonPath)
+ {
+ $this->assertIsArray(self::getUnicodeDocumentCrawler()->find($jsonPath), 'invalid unicode sequence should be treated as literal and not throw');
+ }
+
+ public static function provideInvalidUnicodeSequenceProvider(): array
+ {
+ return [
+ [
+ '$["test\uZZZZ"]',
+ ],
+ [
+ '$["test\u123"]',
+ ],
+ [
+ '$["test\u"]',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideComplexUnicodePath
+ */
+ public function testComplexUnicodePaths(string $jsonPath, array $expected)
+ {
+ $complexJson = [
+ 'データ' => [
+ 'ユーザー' => [
+ ['名前' => 'テスト', 'ID' => 1],
+ ['名前' => 'サンプル', 'ID' => 2],
+ ],
+ ],
+ 'special🔑' => [
+ 'value💎' => 'treasure',
+ ],
+ ];
+
+ $crawler = new JsonCrawler(json_encode($complexJson));
+
+ $this->assertSame($expected, $crawler->find($jsonPath));
+ }
+
+ public static function provideComplexUnicodePath(): array
+ {
+ return [
+ [
+ '$["\u30c7\u30fc\u30bf"]["\u30e6\u30fc\u30b6\u30fc"][0]["\u540d\u524d"]',
+ ['テスト'],
+ ],
+ [
+ '$["special\ud83d\udd11"]["value\ud83d\udc8e"]',
+ ['treasure'],
+ ],
+ [
+ '$["\u30c7\u30fc\u30bf"]["\u30e6\u30fc\u30b6\u30fc"][*]["\u540d\u524d"]',
+ ['テスト', 'サンプル'],
+ ],
+ ];
+ }
+
+ public function testSurrogatePairHandling()
+ {
+ $json = ['𝒽𝑒𝓁𝓁𝑜' => 'mathematical script hello'];
+ $crawler = new JsonCrawler(json_encode($json));
+
+ // mathematical script "hello" requires surrogate pairs for each character
+ $result = $crawler->find('$["\ud835\udcbd\ud835\udc52\ud835\udcc1\ud835\udcc1\ud835\udc5c"]');
+ $this->assertSame(['mathematical script hello'], $result);
+ }
+
+ public function testMixedQuoteTypes()
+ {
+ $json = ['key"with"quotes' => 'value1', "key'with'apostrophes" => 'value2'];
+ $crawler = new JsonCrawler(json_encode($json));
+
+ $result = $crawler->find('$[\'key"with"quotes\']');
+ $this->assertSame(['value1'], $result);
+
+ $result = $crawler->find('$["key\'with\'apostrophes"]');
+ $this->assertSame(['value2'], $result);
+ }
+
private static function getBookstoreCrawler(): JsonCrawler
{
return new JsonCrawler(<< 'coffee',
+ '日本' => 'Japan',
+ 'emoji😀' => 'smiley',
+ 'tab here' => 'with tab',
+ "new\nline" => 'with newline',
+ 'quote"here' => 'with quote',
+ 'backslash\\here' => 'with backslash',
+ 'apostrophe\'here' => 'with apostrophe',
+ "control\x01char" => 'with control char',
+ 'users' => [
+ ['name' => 'café', 'country' => 'France'],
+ ['name' => '日本太郎', 'country' => 'Japan'],
+ ['name' => 'John', 'country' => 'USA'],
+ ['name' => 'Müller', 'country' => 'Germany'],
+ ['name' => 'José', 'country' => 'Spain'],
+ ],
+ ];
+
+ return new JsonCrawler(json_encode($json));
+ }
}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php
new file mode 100644
index 0000000000000..b39b68abcd463
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php
@@ -0,0 +1,224 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\JsonPath\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\JsonPath\Exception\JsonCrawlerException;
+use Symfony\Component\JsonPath\JsonCrawler;
+
+final class JsonPathComplianceTestSuiteTest extends TestCase
+{
+ private const UNSUPPORTED_TEST_CASES = [
+ 'basic, multiple selectors, name and index, object data',
+ 'basic, multiple selectors, index and slice',
+ 'basic, multiple selectors, index and slice, overlapping',
+ 'basic, multiple selectors, wildcard and index',
+ 'basic, multiple selectors, wildcard and name',
+ 'basic, multiple selectors, wildcard and slice',
+ 'basic, multiple selectors, multiple wildcards',
+ 'filter, existence, without segments',
+ 'filter, existence, present with null',
+ 'filter, absolute existence, without segments',
+ 'filter, absolute existence, with segments',
+ 'filter, equals null, absent from data',
+ 'filter, absolute, equals self',
+ 'filter, deep equality, arrays',
+ 'filter, deep equality, objects',
+ 'filter, not-equals string, single quotes',
+ 'filter, not-equals numeric string, single quotes',
+ 'filter, not-equals string, single quotes, different type',
+ 'filter, not-equals string, double quotes',
+ 'filter, not-equals numeric string, double quotes',
+ 'filter, not-equals string, double quotes, different types',
+ 'filter, not-equals null, absent from data',
+ 'filter, less than number',
+ 'filter, less than null',
+ 'filter, less than true',
+ 'filter, less than false',
+ 'filter, less than or equal to true',
+ 'filter, greater than number',
+ 'filter, greater than null',
+ 'filter, greater than true',
+ 'filter, greater than false',
+ 'filter, greater than or equal to string, single quotes',
+ 'filter, greater than or equal to string, double quotes',
+ 'filter, greater than or equal to number',
+ 'filter, greater than or equal to null',
+ 'filter, greater than or equal to true',
+ 'filter, greater than or equal to false',
+ 'filter, exists and not-equals null, absent from data',
+ 'filter, exists and exists, data false',
+ 'filter, exists or exists, data false',
+ 'filter, and',
+ 'filter, or',
+ 'filter, not exists, data null',
+ 'filter, non-singular existence, wildcard',
+ 'filter, non-singular existence, multiple',
+ 'filter, non-singular existence, slice',
+ 'filter, non-singular existence, negated',
+ 'filter, nested',
+ 'filter, name segment on primitive, selects nothing',
+ 'filter, name segment on array, selects nothing',
+ 'filter, index segment on object, selects nothing',
+ 'filter, followed by name selector',
+ 'filter, followed by child segment that selects multiple elements',
+ 'filter, multiple selectors',
+ 'filter, multiple selectors, comparison',
+ 'filter, multiple selectors, overlapping',
+ 'filter, multiple selectors, filter and index',
+ 'filter, multiple selectors, filter and wildcard',
+ 'filter, multiple selectors, filter and slice',
+ 'filter, multiple selectors, comparison filter, index and slice',
+ 'filter, equals number, zero and negative zero',
+ 'filter, equals number, negative zero and zero',
+ 'filter, equals number, with and without decimal fraction',
+ 'filter, equals number, exponent',
+ 'filter, equals number, exponent upper e',
+ 'filter, equals number, positive exponent',
+ 'filter, equals number, negative exponent',
+ 'filter, equals number, exponent 0',
+ 'filter, equals number, exponent -0',
+ 'filter, equals number, exponent +0',
+ 'filter, equals number, exponent leading -0',
+ 'filter, equals number, exponent +00',
+ 'filter, equals number, decimal fraction',
+ 'filter, equals number, decimal fraction, trailing 0',
+ 'filter, equals number, decimal fraction, exponent',
+ 'filter, equals number, decimal fraction, positive exponent',
+ 'filter, equals number, decimal fraction, negative exponent',
+ 'filter, equals, empty node list and empty node list',
+ 'filter, equals, empty node list and special nothing',
+ 'filter, object data',
+ 'filter, and binds more tightly than or',
+ 'filter, left to right evaluation',
+ 'filter, group terms, right',
+ 'name selector, double quotes, escaped reverse solidus',
+ 'name selector, single quotes, escaped reverse solidus',
+ 'slice selector, slice selector with everything omitted, long form',
+ 'slice selector, start, min exact',
+ 'slice selector, start, max exact',
+ 'slice selector, end, min exact',
+ 'slice selector, end, max exact',
+ 'basic, descendant segment, multiple selectors',
+ 'basic, bald descendant segment',
+ 'filter, relative non-singular query, index, equal',
+ 'filter, relative non-singular query, index, not equal',
+ 'filter, relative non-singular query, index, less-or-equal',
+ 'filter, relative non-singular query, name, equal',
+ 'filter, relative non-singular query, name, not equal',
+ 'filter, relative non-singular query, name, less-or-equal',
+ 'filter, relative non-singular query, combined, equal',
+ 'filter, relative non-singular query, combined, not equal',
+ 'filter, relative non-singular query, combined, less-or-equal',
+ 'filter, relative non-singular query, wildcard, equal',
+ 'filter, relative non-singular query, wildcard, not equal',
+ 'filter, relative non-singular query, wildcard, less-or-equal',
+ 'filter, relative non-singular query, slice, equal',
+ 'filter, relative non-singular query, slice, not equal',
+ 'filter, relative non-singular query, slice, less-or-equal',
+ 'filter, absolute non-singular query, index, equal',
+ 'filter, absolute non-singular query, index, not equal',
+ 'filter, absolute non-singular query, index, less-or-equal',
+ 'filter, absolute non-singular query, name, equal',
+ 'filter, absolute non-singular query, name, not equal',
+ 'filter, absolute non-singular query, name, less-or-equal',
+ 'filter, absolute non-singular query, combined, equal',
+ 'filter, absolute non-singular query, combined, not equal',
+ 'filter, absolute non-singular query, combined, less-or-equal',
+ 'filter, absolute non-singular query, wildcard, equal',
+ 'filter, absolute non-singular query, wildcard, not equal',
+ 'filter, absolute non-singular query, wildcard, less-or-equal',
+ 'filter, absolute non-singular query, slice, equal',
+ 'filter, absolute non-singular query, slice, not equal',
+ 'filter, absolute non-singular query, slice, less-or-equal',
+ 'filter, equals, special nothing',
+ 'filter, group terms, left',
+ 'index selector, min exact index - 1',
+ 'index selector, max exact index + 1',
+ 'index selector, overflowing index',
+ 'index selector, leading 0',
+ 'index selector, -0',
+ 'index selector, leading -0',
+ 'name selector, double quotes, escaped line feed',
+ 'name selector, double quotes, invalid escaped single quote',
+ 'name selector, double quotes, question mark escape',
+ 'name selector, double quotes, bell escape',
+ 'name selector, double quotes, vertical tab escape',
+ 'name selector, double quotes, 0 escape',
+ 'name selector, double quotes, x escape',
+ 'name selector, double quotes, n escape',
+ 'name selector, double quotes, unicode escape no hex',
+ 'name selector, double quotes, unicode escape too few hex',
+ 'name selector, double quotes, unicode escape upper u',
+ 'name selector, double quotes, unicode escape upper u long',
+ 'name selector, double quotes, unicode escape plus',
+ 'name selector, double quotes, unicode escape brackets',
+ 'name selector, double quotes, unicode escape brackets long',
+ 'name selector, double quotes, single high surrogate',
+ 'name selector, double quotes, single low surrogate',
+ 'name selector, double quotes, high high surrogate',
+ 'name selector, double quotes, low low surrogate',
+ 'name selector, double quotes, supplementary surrogate',
+ 'name selector, double quotes, surrogate incomplete low',
+ 'name selector, single quotes, escaped backspace',
+ 'name selector, single quotes, escaped line feed',
+ 'name selector, single quotes, invalid escaped double quote',
+ 'slice selector, excessively large from value with negative step',
+ 'slice selector, step, min exact - 1',
+ 'slice selector, step, max exact + 1',
+ 'slice selector, overflowing to value',
+ 'slice selector, underflowing from value',
+ 'slice selector, overflowing from value with negative step',
+ 'slice selector, underflowing to value with negative step',
+ 'slice selector, overflowing step',
+ 'slice selector, underflowing step',
+ 'slice selector, step, leading 0',
+ 'slice selector, step, -0',
+ 'slice selector, step, leading -0',
+ ];
+
+ /**
+ * @dataProvider complianceCaseProvider
+ */
+ public function testComplianceTestCase(string $selector, array $document, array $expectedResults, bool $invalidSelector)
+ {
+ $jsonCrawler = new JsonCrawler(json_encode($document));
+
+ if ($invalidSelector) {
+ $this->expectException(JsonCrawlerException::class);
+ }
+
+ $result = $jsonCrawler->find($selector);
+
+ if (!$invalidSelector) {
+ $this->assertContains($result, $expectedResults);
+ }
+ }
+
+ public static function complianceCaseProvider(): iterable
+ {
+ $data = json_decode(file_get_contents(__DIR__.'/Fixtures/cts.json'), true, flags: \JSON_THROW_ON_ERROR);
+
+ foreach ($data['tests'] as $test) {
+ if (\in_array($test['name'], self::UNSUPPORTED_TEST_CASES, true)) {
+ continue;
+ }
+
+ yield $test['name'] => [
+ $test['selector'],
+ $test['document'] ?? [],
+ isset($test['result']) ? [$test['result']] : ($test['results'] ?? []),
+ $test['invalid_selector'] ?? false,
+ ];
+ }
+ }
+}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php b/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
index 52d05bdaeb813..cbe6f20d17c0b 100644
--- a/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
@@ -23,8 +23,8 @@ public function testBuildPath()
->index(0)
->key('address');
- $this->assertSame('$.users[0].address', (string) $path);
- $this->assertSame('$.users[0].address..city', (string) $path->deepScan()->key('city'));
+ $this->assertSame('$["users"][0]["address"]', (string) $path);
+ $this->assertSame('$["users"][0]["address"]..["city"]', (string) $path->deepScan()->key('city'));
}
public function testBuildWithFilter()
@@ -33,7 +33,7 @@ public function testBuildWithFilter()
$path = $path->key('users')
->filter('@.age > 18');
- $this->assertSame('$.users[?(@.age > 18)]', (string) $path);
+ $this->assertSame('$["users"][?(@.age > 18)]', (string) $path);
}
public function testAll()
@@ -42,7 +42,7 @@ public function testAll()
$path = $path->key('users')
->all();
- $this->assertSame('$.users[*]', (string) $path);
+ $this->assertSame('$["users"][*]', (string) $path);
}
public function testFirst()
@@ -51,7 +51,7 @@ public function testFirst()
$path = $path->key('users')
->first();
- $this->assertSame('$.users[0]', (string) $path);
+ $this->assertSame('$["users"][0]', (string) $path);
}
public function testLast()
@@ -60,6 +60,47 @@ public function testLast()
$path = $path->key('users')
->last();
- $this->assertSame('$.users[-1]', (string) $path);
+ $this->assertSame('$["users"][-1]', (string) $path);
+ }
+
+ /**
+ * @dataProvider provideKeysToEscape
+ */
+ public function testEscapedKey(string $key, string $expectedPath)
+ {
+ $path = new JsonPath();
+ $path = $path->key($key);
+
+ $this->assertSame($expectedPath, (string) $path);
+ }
+
+ public static function provideKeysToEscape(): iterable
+ {
+ yield ['simple_key', '$["simple_key"]'];
+ yield ['key"with"quotes', '$["key\\"with\\"quotes"]'];
+ yield ['path\\backslash', '$["path\\backslash"]'];
+ yield ['mixed\\"case', '$["mixed\\\\\\"case"]'];
+ yield ['unicode_🔑', '$["unicode_🔑"]'];
+ yield ['"quotes_only"', '$["\\"quotes_only\\""]'];
+ yield ['\\\\multiple\\\\backslashes', '$["\\\\\\\\multiple\\\\\\backslashes"]'];
+ yield ["control\x00\x1f\x1echar", '$["control\u0000\u001f\u001echar"]'];
+
+ yield ['key"with\\"mixed', '$["key\\"with\\\\\\"mixed"]'];
+ yield ['\\"complex\\"case\\"', '$["\\\\\\"complex\\\\\\"case\\\\\\""]'];
+ yield ['json_like":{"value":"test"}', '$["json_like\\":{\\"value\\":\\"test\\"}"]'];
+ yield ['C:\\Program Files\\"App Name"', '$["C:\\\\Program Files\\\\\\"App Name\\""]'];
+
+ yield ['key_with_é_accents', '$["key_with_é_accents"]'];
+ yield ['unicode_→_arrows', '$["unicode_→_arrows"]'];
+ yield ['chinese_中文_key', '$["chinese_中文_key"]'];
+
+ yield ['', '$[""]'];
+ yield [' ', '$[" "]'];
+ yield [' spaces ', '$[" spaces "]'];
+ yield ["\t\n\r", '$["\\t\\n\\r"]'];
+ yield ["control\x00char", '$["control\u0000char"]'];
+ yield ["newline\nkey", '$["newline\\nkey"]'];
+ yield ["tab\tkey", '$["tab\\tkey"]'];
+ yield ["carriage\rreturn", '$["carriage\\rreturn"]'];
}
}
diff --git a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
index 62d64b53e1e8d..1044e7658672b 100644
--- a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
@@ -1,5 +1,14 @@
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
namespace Symfony\Component\JsonPath\Tests\Test;
use PHPUnit\Framework\AssertionFailedError;
diff --git a/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php b/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
index b6768ff7ac9db..fdbd36d3cbc36 100644
--- a/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
@@ -355,9 +355,7 @@ public static function provideInvalidUtf8PropertyName(): array
'special char first' => ['#test'],
'start with digit' => ['123test'],
'asterisk' => ['test*test'],
- 'space not allowed' => [' test'],
'at sign not allowed' => ['@test'],
- 'start control char' => ["\0test"],
'ending control char' => ["test\xFF\xFA"],
'dash sign' => ['-test'],
];
diff --git a/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php b/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
index d7c5fe44457e7..e9ca872f223b9 100644
--- a/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
+++ b/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
@@ -13,6 +13,7 @@
use Symfony\Component\JsonPath\Exception\InvalidJsonPathException;
use Symfony\Component\JsonPath\JsonPath;
+use Symfony\Component\JsonPath\JsonPathUtils;
/**
* @author Alexandre Daubois
@@ -21,6 +22,9 @@
*/
final class JsonPathTokenizer
{
+ private const RFC9535_WHITESPACE_CHARS = [' ', "\t", "\n", "\r"];
+ private const BARE_LITERAL_REGEX = '(true|false|null|\d+(\.\d+)?([eE][+-]?\d+)?|\'[^\']*\'|"[^"]*")';
+
/**
* @return JsonPathToken[]
*/
@@ -34,6 +38,8 @@ public static function tokenize(JsonPath $query): array
$inQuote = false;
$quoteChar = '';
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
+ $hasContentAfterRoot = false;
$chars = mb_str_split((string) $query);
$length = \count($chars);
@@ -42,14 +48,36 @@ public static function tokenize(JsonPath $query): array
throw new InvalidJsonPathException('empty JSONPath expression.');
}
- if ('$' !== $chars[0]) {
+ $i = self::skipWhitespace($chars, 0, $length);
+ if ($i >= $length || '$' !== $chars[$i]) {
throw new InvalidJsonPathException('expression must start with $.');
}
+ $rootIndex = $i;
+ if ($rootIndex + 1 < $length) {
+ $hasContentAfterRoot = true;
+ }
+
for ($i = 0; $i < $length; ++$i) {
$char = $chars[$i];
$position = $i;
+ if (!$inQuote && !$inBracket && self::isWhitespace($char)) {
+ if ('' !== $current) {
+ $tokens[] = new JsonPathToken(TokenType::Name, $current);
+ $current = '';
+ }
+
+ $nextNonWhitespaceIndex = self::skipWhitespace($chars, $i, $length);
+ if ($nextNonWhitespaceIndex < $length && '[' !== $chars[$nextNonWhitespaceIndex] && '.' !== $chars[$nextNonWhitespaceIndex]) {
+ throw new InvalidJsonPathException('whitespace is not allowed in property names.', $i);
+ }
+
+ $i = $nextNonWhitespaceIndex - 1;
+
+ continue;
+ }
+
if (('"' === $char || "'" === $char) && !$inQuote) {
$inQuote = true;
$quoteChar = $char;
@@ -58,10 +86,32 @@ public static function tokenize(JsonPath $query): array
}
if ($inQuote) {
+ // literal control characters (U+0000 through U+001F) in quoted strings
+ // are not be allowed unless they are part of escape sequences
+ $ord = \ord($char);
+ if ($inBracket) {
+ if ($ord <= 31) {
+ $isEscapedChar = ($i > 0 && '\\' === $chars[$i - 1]);
+
+ if (!$isEscapedChar) {
+ throw new InvalidJsonPathException('control characters are not allowed in quoted strings.', $position);
+ }
+ }
+
+ if ("\n" === $char && $i > 0 && '\\' === $chars[$i - 1]) {
+ throw new InvalidJsonPathException('escaped newlines are not allowed in quoted strings.', $position);
+ }
+
+ if ('u' === $char && $i > 0 && '\\' === $chars[$i - 1]) {
+ self::validateUnicodeEscape($chars, $i, $position);
+ }
+ }
+
$current .= $char;
- if ($char === $quoteChar && '\\' !== $chars[$i - 1]) {
+ if ($char === $quoteChar && (0 === $i || '\\' !== $chars[$i - 1])) {
$inQuote = false;
}
+
if ($i === $length - 1 && $inQuote) {
throw new InvalidJsonPathException('unclosed string literal.', $position);
}
@@ -80,11 +130,22 @@ public static function tokenize(JsonPath $query): array
$inBracket = true;
++$bracketDepth;
+ $i = self::skipWhitespace($chars, $i + 1, $length) - 1; // -1 because loop will increment
+
+ continue;
+ }
+
+ if ('[' === $char && $inFilter) {
+ // inside filter expressions, brackets are part of the filter content
+ ++$filterBracketDepth;
+ $current .= $char;
continue;
}
if (']' === $char) {
- if ($inFilter && $filterParenthesisDepth > 0) {
+ if ($inFilter && $filterBracketDepth > 0) {
+ // inside filter expressions, brackets are part of the filter content
+ --$filterBracketDepth;
$current .= $char;
continue;
}
@@ -94,35 +155,61 @@ public static function tokenize(JsonPath $query): array
}
if (0 === $bracketDepth) {
- if ('' === $current) {
+ if ('' === $current = trim($current)) {
throw new InvalidJsonPathException('empty brackets are not allowed.', $position);
}
+ // validate filter expressions
+ if (str_starts_with($current, '?')) {
+ if ($filterParenthesisDepth > 0) {
+ throw new InvalidJsonPathException('unclosed bracket.', $position);
+ }
+ self::validateFilterExpression($current, $position);
+ }
+
$tokens[] = new JsonPathToken(TokenType::Bracket, $current);
$current = '';
$inBracket = false;
$inFilter = false;
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
continue;
}
}
if ('?' === $char && $inBracket && !$inFilter) {
- if ('' !== $current) {
+ if ('' !== trim($current)) {
throw new InvalidJsonPathException('unexpected characters before filter expression.', $position);
}
+
+ $current = '?';
$inFilter = true;
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
+
+ continue;
}
if ($inFilter) {
if ('(' === $char) {
+ if (preg_match('/\w\s+$/', $current)) {
+ throw new InvalidJsonPathException('whitespace is not allowed between function name and parenthesis.', $position);
+ }
++$filterParenthesisDepth;
} elseif (')' === $char) {
if (--$filterParenthesisDepth < 0) {
throw new InvalidJsonPathException('unmatched closing parenthesis in filter.', $position);
}
}
+ $current .= $char;
+
+ continue;
+ }
+
+ if ($inBracket && self::isWhitespace($char)) {
+ $current .= $char;
+
+ continue;
}
// recursive descent
@@ -158,7 +245,7 @@ public static function tokenize(JsonPath $query): array
throw new InvalidJsonPathException('unclosed string literal.', $length - 1);
}
- if ('' !== $current) {
+ if ('' !== $current = trim($current)) {
// final validation of the whole name
if (!preg_match('/^(?:\*|[a-zA-Z_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}][a-zA-Z0-9_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}]*)$/u', $current)) {
throw new InvalidJsonPathException(\sprintf('invalid character in property name "%s"', $current));
@@ -167,6 +254,230 @@ public static function tokenize(JsonPath $query): array
$tokens[] = new JsonPathToken(TokenType::Name, $current);
}
+ if ($hasContentAfterRoot && !$tokens) {
+ throw new InvalidJsonPathException('invalid JSONPath expression.');
+ }
+
return $tokens;
}
+
+ private static function isWhitespace(string $char): bool
+ {
+ return \in_array($char, self::RFC9535_WHITESPACE_CHARS, true);
+ }
+
+ private static function skipWhitespace(array $chars, int $index, int $length): int
+ {
+ while ($index < $length && self::isWhitespace($chars[$index])) {
+ ++$index;
+ }
+
+ return $index;
+ }
+
+ private static function validateFilterExpression(string $expr, int $position): void
+ {
+ self::validateBareLiterals($expr, $position);
+
+ $filterExpr = ltrim($expr, '?');
+ $filterExpr = trim($filterExpr);
+
+ $comparisonOps = ['==', '!=', '>=', '<=', '>', '<'];
+ foreach ($comparisonOps as $op) {
+ if (str_contains($filterExpr, $op)) {
+ [$left, $right] = array_map('trim', explode($op, $filterExpr, 2));
+
+ // check if either side contains non-singular queries
+ if (self::isNonSingularQuery($left) || self::isNonSingularQuery($right)) {
+ throw new InvalidJsonPathException('Non-singular query is not comparable.', $position);
+ }
+
+ break;
+ }
+ }
+
+ // look for invalid number formats in filter expressions
+ $operators = [...$comparisonOps, '&&', '||'];
+ $tokens = [$filterExpr];
+
+ foreach ($operators as $op) {
+ $newTokens = [];
+ foreach ($tokens as $token) {
+ $newTokens = array_merge($newTokens, explode($op, $token));
+ }
+
+ $tokens = $newTokens;
+ }
+
+ foreach ($tokens as $token) {
+ if (
+ '' === ($token = trim($token))
+ || \in_array($token, ['true', 'false', 'null'], true)
+ || false !== strpbrk($token[0], '@"\'')
+ || false !== strpbrk($token, '()[]$')
+ || (str_contains($token, '.') && !preg_match('/^[\d+\-.eE\s]*\./', $token))
+ ) {
+ continue;
+ }
+
+ // strict JSON number format validation
+ if (
+ preg_match('/^(?=[\d+\-.eE\s]+$)(?=.*\d)/', $token)
+ && !preg_match('/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/', $token)
+ ) {
+ throw new InvalidJsonPathException(\sprintf('Invalid number format "%s" in filter expression.', $token), $position);
+ }
+ }
+ }
+
+ private static function validateBareLiterals(string $expr, int $position): void
+ {
+ $filterExpr = ltrim($expr, '?');
+ $filterExpr = trim($filterExpr);
+
+ if (preg_match('/\b(True|False|Null)\b/', $filterExpr)) {
+ throw new InvalidJsonPathException('Incorrectly capitalized literal in filter expression.', $position);
+ }
+
+ if (preg_match('/^(length|count|value)\s*\([^)]*\)$/', $filterExpr)) {
+ throw new InvalidJsonPathException('Function result must be compared.', $position);
+ }
+
+ if (preg_match('/\b(length|count|value)\s*\(([^)]*)\)/', $filterExpr, $matches)) {
+ $functionName = $matches[1];
+ $args = trim($matches[2]);
+ if (!$args) {
+ throw new InvalidJsonPathException('Function requires exactly one argument.', $position);
+ }
+
+ $argParts = JsonPathUtils::parseCommaSeparatedValues($args);
+ if (1 !== \count($argParts)) {
+ throw new InvalidJsonPathException('Function requires exactly one argument.', $position);
+ }
+
+ $arg = trim($argParts[0]);
+
+ if ('count' === $functionName && preg_match('/^'.self::BARE_LITERAL_REGEX.'$/', $arg)) {
+ throw new InvalidJsonPathException('count() function requires a query argument, not a literal.', $position);
+ }
+
+ if ('length' === $functionName && preg_match('/@\.\*/', $arg)) {
+ throw new InvalidJsonPathException('Function argument must be a singular query.', $position);
+ }
+ }
+
+ if (preg_match('/\b(match|search)\s*\(([^)]*)\)/', $filterExpr, $matches)) {
+ $args = trim($matches[2]);
+ if (!$args) {
+ throw new InvalidJsonPathException('Function requires exactly two arguments.', $position);
+ }
+
+ $argParts = JsonPathUtils::parseCommaSeparatedValues($args);
+ if (2 !== \count($argParts)) {
+ throw new InvalidJsonPathException('Function requires exactly two arguments.', $position);
+ }
+ }
+
+ if (preg_match('/^'.self::BARE_LITERAL_REGEX.'$/', $filterExpr)) {
+ throw new InvalidJsonPathException('Bare literal in filter expression - literals must be compared.', $position);
+ }
+
+ if (preg_match('/\b'.self::BARE_LITERAL_REGEX.'\s*(&&|\|\|)\s*'.self::BARE_LITERAL_REGEX.'\b/', $filterExpr)) {
+ throw new InvalidJsonPathException('Bare literals in logical expression - literals must be compared.', $position);
+ }
+
+ if (preg_match('/\b(match|search|length|count|value)\s*\([^)]*\)\s*[=!]=\s*(true|false)\b/', $filterExpr)
+ || preg_match('/\b(true|false)\s*[=!]=\s*(match|search|length|count|value)\s*\([^)]*\)/', $filterExpr)) {
+ throw new InvalidJsonPathException('Function result cannot be compared to boolean literal.', $position);
+ }
+
+ if (preg_match('/\b'.self::BARE_LITERAL_REGEX.'\s*(&&|\|\|)/', $filterExpr)
+ || preg_match('/(&&|\|\|)\s*'.self::BARE_LITERAL_REGEX.'\b/', $filterExpr)) {
+ // check if the literal is not part of a comparison
+ if (!preg_match('/(@[^=<>!]*|[^=<>!@]+)\s*[=<>!]+\s*'.self::BARE_LITERAL_REGEX.'/', $filterExpr)
+ && !preg_match('/'.self::BARE_LITERAL_REGEX.'\s*[=<>!]+\s*(@[^=<>!]*|[^=<>!@]+)/', $filterExpr)
+ ) {
+ throw new InvalidJsonPathException('Bare literal in logical expression - literals must be compared.', $position);
+ }
+ }
+ }
+
+ private static function isNonSingularQuery(string $query): bool
+ {
+ if (!str_starts_with($query = trim($query), '@')) {
+ return false;
+ }
+
+ if (preg_match('/@(\.\.)|(.*\[\*])|(.*\.\*)|(.*\[.*:.*])|(.*\[.*,.*])/', $query)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static function validateUnicodeEscape(array $chars, int $index, int $position): void
+ {
+ if ($index + 4 >= \count($chars)) {
+ return;
+ }
+
+ $hexDigits = '';
+ for ($i = 1; $i <= 4; ++$i) {
+ $hexDigits .= $chars[$index + $i];
+ }
+
+ if (!preg_match('/^[0-9A-Fa-f]{4}$/', $hexDigits)) {
+ return;
+ }
+
+ $codePoint = hexdec($hexDigits);
+
+ if ($codePoint >= 0xD800 && $codePoint <= 0xDBFF) {
+ $nextIndex = $index + 5;
+
+ if ($nextIndex + 1 < \count($chars)
+ && '\\' === $chars[$nextIndex] && 'u' === $chars[$nextIndex + 1]
+ ) {
+ $nextHexDigits = '';
+ for ($i = 2; $i <= 5; ++$i) {
+ $nextHexDigits .= $chars[$nextIndex + $i];
+ }
+
+ if (preg_match('/^[0-9A-Fa-f]{4}$/', $nextHexDigits)) {
+ $nextCodePoint = hexdec($nextHexDigits);
+
+ // high surrogate must be followed by low surrogate
+ if ($nextCodePoint < 0xDC00 || $nextCodePoint > 0xDFFF) {
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ } else {
+ // high surrogate not followed by low surrogate
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ } elseif ($codePoint >= 0xDC00 && $codePoint <= 0xDFFF) {
+ $prevIndex = $index - 7; // position of \ in previous \uXXXX (7 positions back: u+4hex+\+u)
+
+ if ($prevIndex >= 0
+ && '\\' === $chars[$prevIndex] && 'u' === $chars[$prevIndex + 1]
+ ) {
+ $prevHexDigits = '';
+ for ($i = 2; $i <= 5; ++$i) {
+ $prevHexDigits .= $chars[$prevIndex + $i];
+ }
+
+ if (preg_match('/^[0-9A-Fa-f]{4}$/', $prevHexDigits)) {
+ $prevCodePoint = hexdec($prevHexDigits);
+
+ // low surrogate must be preceded by high surrogate
+ if ($prevCodePoint < 0xD800 || $prevCodePoint > 0xDBFF) {
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ } else {
+ // low surrogate not preceded by high surrogate
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ }
}
diff --git a/src/Symfony/Component/JsonPath/composer.json b/src/Symfony/Component/JsonPath/composer.json
index fe8ddf84dd82d..809739d2eaa11 100644
--- a/src/Symfony/Component/JsonPath/composer.json
+++ b/src/Symfony/Component/JsonPath/composer.json
@@ -17,13 +17,11 @@
],
"require": {
"php": ">=8.2",
+ "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
- "symfony/json-streamer": "7.3.*"
- },
- "conflict": {
- "symfony/json-streamer": ">=7.4"
+ "symfony/json-streamer": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\JsonPath\\": "" },
diff --git a/src/Symfony/Component/JsonStreamer/CHANGELOG.md b/src/Symfony/Component/JsonStreamer/CHANGELOG.md
index 5294c5b5f3637..87f1e74c951da 100644
--- a/src/Symfony/Component/JsonStreamer/CHANGELOG.md
+++ b/src/Symfony/Component/JsonStreamer/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Remove `nikic/php-parser` dependency
+
7.3
---
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php b/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php
deleted file mode 100644
index 99f3dbfd0e9b8..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php
+++ /dev/null
@@ -1,29 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\Node\Expr;
-
-/**
- * Represents a way to access data on PHP.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-interface DataAccessorInterface
-{
- /**
- * Converts to "nikic/php-parser" PHP expression.
- */
- public function toPhpExpr(): Expr;
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php
deleted file mode 100644
index 8ad8960674d57..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php
+++ /dev/null
@@ -1,57 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node\Expr;
-
-/**
- * Defines the way to access data using a function (or a method).
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class FunctionDataAccessor implements DataAccessorInterface
-{
- /**
- * @param list $arguments
- */
- public function __construct(
- private string $functionName,
- private array $arguments,
- private ?DataAccessorInterface $objectAccessor = null,
- ) {
- }
-
- public function getObjectAccessor(): ?DataAccessorInterface
- {
- return $this->objectAccessor;
- }
-
- public function withObjectAccessor(?DataAccessorInterface $accessor): self
- {
- return new self($this->functionName, $this->arguments, $accessor);
- }
-
- public function toPhpExpr(): Expr
- {
- $builder = new BuilderFactory();
- $arguments = array_map(static fn (DataAccessorInterface $argument): Expr => $argument->toPhpExpr(), $this->arguments);
-
- if (null === $this->objectAccessor) {
- return $builder->funcCall($this->functionName, $arguments);
- }
-
- return $builder->methodCall($this->objectAccessor->toPhpExpr(), $this->functionName, $arguments);
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php
deleted file mode 100644
index 9806b94ed0a9f..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php
+++ /dev/null
@@ -1,34 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\Node\Expr;
-
-/**
- * Defines the way to access data using PHP AST.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class PhpExprDataAccessor implements DataAccessorInterface
-{
- public function __construct(
- private Expr $php,
- ) {
- }
-
- public function toPhpExpr(): Expr
- {
- return $this->php;
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php
deleted file mode 100644
index f48c98064bb65..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php
+++ /dev/null
@@ -1,46 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node\Expr;
-
-/**
- * Defines the way to access data using an object property.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class PropertyDataAccessor implements DataAccessorInterface
-{
- public function __construct(
- private DataAccessorInterface $objectAccessor,
- private string $propertyName,
- ) {
- }
-
- public function getObjectAccessor(): DataAccessorInterface
- {
- return $this->objectAccessor;
- }
-
- public function withObjectAccessor(DataAccessorInterface $accessor): self
- {
- return new self($accessor, $this->propertyName);
- }
-
- public function toPhpExpr(): Expr
- {
- return (new BuilderFactory())->propertyFetch($this->objectAccessor->toPhpExpr(), $this->propertyName);
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php
index 25d53c15fff60..e1a7e68927a6e 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Read;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\UnionType;
@@ -25,7 +24,7 @@
final class ObjectNode implements DataModelNodeInterface
{
/**
- * @param array $properties
+ * @param array $properties
*/
public function __construct(
private ObjectType $type,
@@ -50,7 +49,7 @@ public function getType(): ObjectType
}
/**
- * @return array
+ * @return array
*/
public function getProperties(): array
{
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php
deleted file mode 100644
index f60220dd82e7a..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php
+++ /dev/null
@@ -1,35 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node\Expr;
-
-/**
- * Defines the way to access a scalar value.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class ScalarDataAccessor implements DataAccessorInterface
-{
- public function __construct(
- private mixed $value,
- ) {
- }
-
- public function toPhpExpr(): Expr
- {
- return (new BuilderFactory())->val($this->value);
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php
deleted file mode 100644
index 0046f55b4e7e0..0000000000000
--- a/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php
+++ /dev/null
@@ -1,35 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\DataModel;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node\Expr;
-
-/**
- * Defines the way to access data using a variable.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class VariableDataAccessor implements DataAccessorInterface
-{
- public function __construct(
- private string $name,
- ) {
- }
-
- public function toPhpExpr(): Expr
- {
- return (new BuilderFactory())->var($this->name);
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php
index ba96b98319d1e..5a3b74861c3cd 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
/**
@@ -26,12 +25,12 @@
final class BackedEnumNode implements DataModelNodeInterface
{
public function __construct(
- private DataAccessorInterface $accessor,
+ private string $accessor,
private BackedEnumType $type,
) {
}
- public function withAccessor(DataAccessorInterface $accessor): self
+ public function withAccessor(string $accessor): self
{
return new self($accessor, $this->type);
}
@@ -41,7 +40,7 @@ public function getIdentifier(): string
return (string) $this->getType();
}
- public function getAccessor(): DataAccessorInterface
+ public function getAccessor(): string
{
return $this->accessor;
}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php
index 2f324fb404908..a334437c6891b 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\TypeInfo\Type\CollectionType;
/**
@@ -24,13 +23,13 @@
final class CollectionNode implements DataModelNodeInterface
{
public function __construct(
- private DataAccessorInterface $accessor,
+ private string $accessor,
private CollectionType $type,
private DataModelNodeInterface $item,
) {
}
- public function withAccessor(DataAccessorInterface $accessor): self
+ public function withAccessor(string $accessor): self
{
return new self($accessor, $this->type, $this->item);
}
@@ -40,7 +39,7 @@ public function getIdentifier(): string
return (string) $this->getType();
}
- public function getAccessor(): DataAccessorInterface
+ public function getAccessor(): string
{
return $this->accessor;
}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php
index 705d610fe7932..2469fbfb0e14c 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\UnionType;
@@ -43,7 +42,7 @@ final class CompositeNode implements DataModelNodeInterface
* @param list $nodes
*/
public function __construct(
- private DataAccessorInterface $accessor,
+ private string $accessor,
array $nodes,
) {
if (\count($nodes) < 2) {
@@ -60,7 +59,7 @@ public function __construct(
$this->nodes = $nodes;
}
- public function withAccessor(DataAccessorInterface $accessor): self
+ public function withAccessor(string $accessor): self
{
return new self($accessor, array_map(static fn (DataModelNodeInterface $n): DataModelNodeInterface => $n->withAccessor($accessor), $this->nodes));
}
@@ -70,7 +69,7 @@ public function getIdentifier(): string
return (string) $this->getType();
}
- public function getAccessor(): DataAccessorInterface
+ public function getAccessor(): string
{
return $this->accessor;
}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php
index fa94649cda40a..7768cd4179a85 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\TypeInfo\Type;
/**
@@ -27,7 +26,7 @@ public function getIdentifier(): string;
public function getType(): Type;
- public function getAccessor(): DataAccessorInterface;
+ public function getAccessor(): string;
- public function withAccessor(DataAccessorInterface $accessor): self;
+ public function withAccessor(string $accessor): self;
}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php
index 56dfcad38c0fe..1f8f79a171067 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php
@@ -11,9 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
-use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor;
use Symfony\Component\TypeInfo\Type\ObjectType;
/**
@@ -29,29 +26,23 @@ final class ObjectNode implements DataModelNodeInterface
* @param array $properties
*/
public function __construct(
- private DataAccessorInterface $accessor,
+ private string $accessor,
private ObjectType $type,
private array $properties,
private bool $mock = false,
) {
}
- public static function createMock(DataAccessorInterface $accessor, ObjectType $type): self
+ public static function createMock(string $accessor, ObjectType $type): self
{
return new self($accessor, $type, [], true);
}
- public function withAccessor(DataAccessorInterface $accessor): self
+ public function withAccessor(string $accessor): self
{
$properties = [];
foreach ($this->properties as $key => $property) {
- $propertyAccessor = $property->getAccessor();
-
- if ($propertyAccessor instanceof PropertyDataAccessor || $propertyAccessor instanceof FunctionDataAccessor && $propertyAccessor->getObjectAccessor()) {
- $propertyAccessor = $propertyAccessor->withObjectAccessor($accessor);
- }
-
- $properties[$key] = $property->withAccessor($propertyAccessor);
+ $properties[$key] = $property->withAccessor(str_replace($this->accessor, $accessor, $property->getAccessor()));
}
return new self($accessor, $this->type, $properties, $this->mock);
@@ -62,7 +53,7 @@ public function getIdentifier(): string
return (string) $this->getType();
}
- public function getAccessor(): DataAccessorInterface
+ public function getAccessor(): string
{
return $this->accessor;
}
diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php
index 53dc88b321d3f..d40319e0e5013 100644
--- a/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php
+++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\JsonStreamer\DataModel\Write;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\TypeInfo\Type\BuiltinType;
/**
@@ -26,12 +25,12 @@
final class ScalarNode implements DataModelNodeInterface
{
public function __construct(
- private DataAccessorInterface $accessor,
+ private string $accessor,
private BuiltinType $type,
) {
}
- public function withAccessor(DataAccessorInterface $accessor): self
+ public function withAccessor(string $accessor): self
{
return new self($accessor, $this->type);
}
@@ -41,7 +40,7 @@ public function getIdentifier(): string
return (string) $this->getType();
}
- public function getAccessor(): DataAccessorInterface
+ public function getAccessor(): string
{
return $this->accessor;
}
diff --git a/src/Symfony/Component/JsonStreamer/JsonStreamReader.php b/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
index b2f2fabaa3dad..e813f4a8a5408 100644
--- a/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
+++ b/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
@@ -45,7 +45,7 @@ public function __construct(
private ContainerInterface $valueTransformers,
PropertyMetadataLoaderInterface $propertyMetadataLoader,
string $streamReadersDir,
- string $lazyGhostsDir,
+ ?string $lazyGhostsDir = null,
) {
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir);
$this->instantiator = new Instantiator();
diff --git a/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php b/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php
deleted file mode 100644
index 7a6e23762beca..0000000000000
--- a/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php
+++ /dev/null
@@ -1,590 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\Read;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node;
-use PhpParser\Node\Expr;
-use PhpParser\Node\Expr\Array_;
-use PhpParser\Node\Expr\ArrayDimFetch;
-use PhpParser\Node\Expr\ArrayItem;
-use PhpParser\Node\Expr\Assign;
-use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
-use PhpParser\Node\Expr\BinaryOp\Coalesce;
-use PhpParser\Node\Expr\BinaryOp\Identical;
-use PhpParser\Node\Expr\BinaryOp\NotIdentical;
-use PhpParser\Node\Expr\Cast\Object_ as ObjectCast;
-use PhpParser\Node\Expr\Cast\String_ as StringCast;
-use PhpParser\Node\Expr\ClassConstFetch;
-use PhpParser\Node\Expr\Closure;
-use PhpParser\Node\Expr\ClosureUse;
-use PhpParser\Node\Expr\Match_;
-use PhpParser\Node\Expr\Ternary;
-use PhpParser\Node\Expr\Throw_;
-use PhpParser\Node\Expr\Yield_;
-use PhpParser\Node\Identifier;
-use PhpParser\Node\MatchArm;
-use PhpParser\Node\Name\FullyQualified;
-use PhpParser\Node\Param;
-use PhpParser\Node\Stmt;
-use PhpParser\Node\Stmt\Expression;
-use PhpParser\Node\Stmt\Foreach_;
-use PhpParser\Node\Stmt\If_;
-use PhpParser\Node\Stmt\Return_;
-use Psr\Container\ContainerInterface;
-use Symfony\Component\JsonStreamer\DataModel\PhpExprDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
-use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode;
-use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode;
-use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface;
-use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode;
-use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode;
-use Symfony\Component\JsonStreamer\Exception\LogicException;
-use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException;
-use Symfony\Component\TypeInfo\Type\BackedEnumType;
-use Symfony\Component\TypeInfo\Type\BuiltinType;
-use Symfony\Component\TypeInfo\Type\CollectionType;
-use Symfony\Component\TypeInfo\Type\ObjectType;
-use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
-use Symfony\Component\TypeInfo\TypeIdentifier;
-
-/**
- * Builds a PHP syntax tree that reads JSON stream.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class PhpAstBuilder
-{
- private BuilderFactory $builder;
-
- public function __construct()
- {
- $this->builder = new BuilderFactory();
- }
-
- /**
- * @param array $options
- * @param array $context
- *
- * @return list
- */
- public function build(DataModelNodeInterface $dataModel, bool $decodeFromStream, array $options = [], array $context = []): array
- {
- if ($decodeFromStream) {
- return [new Return_(new Closure([
- 'static' => true,
- 'params' => [
- new Param($this->builder->var('stream'), type: new Identifier('mixed')),
- new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)),
- new Param($this->builder->var('instantiator'), type: new FullyQualified(LazyInstantiator::class)),
- new Param($this->builder->var('options'), type: new Identifier('array')),
- ],
- 'returnType' => new Identifier('mixed'),
- 'stmts' => [
- ...$this->buildProvidersStatements($dataModel, $decodeFromStream, $context),
- new Return_(
- $this->nodeOnlyNeedsDecode($dataModel, $decodeFromStream)
- ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [
- $this->builder->var('stream'),
- $this->builder->val(0),
- $this->builder->val(null),
- ])
- : $this->builder->funcCall(new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($dataModel->getIdentifier())), [
- $this->builder->var('stream'),
- $this->builder->val(0),
- $this->builder->val(null),
- ]),
- ),
- ],
- ]))];
- }
-
- return [new Return_(new Closure([
- 'static' => true,
- 'params' => [
- new Param($this->builder->var('string'), type: new Identifier('string|\\Stringable')),
- new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)),
- new Param($this->builder->var('instantiator'), type: new FullyQualified(Instantiator::class)),
- new Param($this->builder->var('options'), type: new Identifier('array')),
- ],
- 'returnType' => new Identifier('mixed'),
- 'stmts' => [
- ...$this->buildProvidersStatements($dataModel, $decodeFromStream, $context),
- new Return_(
- $this->nodeOnlyNeedsDecode($dataModel, $decodeFromStream)
- ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeString', [new StringCast($this->builder->var('string'))])
- : $this->builder->funcCall(new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($dataModel->getIdentifier())), [
- $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeString', [new StringCast($this->builder->var('string'))]),
- ]),
- ),
- ],
- ]))];
- }
-
- /**
- * @param array $context
- *
- * @return list
- */
- private function buildProvidersStatements(DataModelNodeInterface $node, bool $decodeFromStream, array &$context): array
- {
- if ($context['providers'][$node->getIdentifier()] ?? false) {
- return [];
- }
-
- $context['providers'][$node->getIdentifier()] = true;
-
- if ($this->nodeOnlyNeedsDecode($node, $decodeFromStream)) {
- return [];
- }
-
- return match (true) {
- $node instanceof ScalarNode || $node instanceof BackedEnumNode => $this->buildLeafProviderStatements($node, $decodeFromStream),
- $node instanceof CompositeNode => $this->buildCompositeNodeStatements($node, $decodeFromStream, $context),
- $node instanceof CollectionNode => $this->buildCollectionNodeStatements($node, $decodeFromStream, $context),
- $node instanceof ObjectNode => $this->buildObjectNodeStatements($node, $decodeFromStream, $context),
- default => throw new LogicException(\sprintf('Unexpected "%s" data model node.', $node::class)),
- };
- }
-
- /**
- * @return list
- */
- private function buildLeafProviderStatements(ScalarNode|BackedEnumNode $node, bool $decodeFromStream): array
- {
- $accessor = $decodeFromStream
- ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [
- $this->builder->var('stream'),
- $this->builder->var('offset'),
- $this->builder->var('length'),
- ])
- : $this->builder->var('data');
-
- $params = $decodeFromStream
- ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))]
- : [new Param($this->builder->var('data'))];
-
- return [
- new Expression(new Assign(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())),
- new Closure([
- 'static' => true,
- 'params' => $params,
- 'stmts' => [new Return_($this->buildFormatValueStatement($node, $accessor))],
- ]),
- )),
- ];
- }
-
- private function buildFormatValueStatement(DataModelNodeInterface $node, Expr $accessor): Node
- {
- if ($node instanceof BackedEnumNode) {
- /** @var ObjectType $type */
- $type = $node->getType();
-
- return $this->builder->staticCall(new FullyQualified($type->getClassName()), 'from', [$accessor]);
- }
-
- if ($node instanceof ScalarNode) {
- /** @var BuiltinType $type */
- $type = $node->getType();
-
- return match (true) {
- TypeIdentifier::NULL === $type->getTypeIdentifier() => $this->builder->val(null),
- TypeIdentifier::OBJECT === $type->getTypeIdentifier() => new ObjectCast($accessor),
- default => $accessor,
- };
- }
-
- return $accessor;
- }
-
- /**
- * @param array $context
- *
- * @return list
- */
- private function buildCompositeNodeStatements(CompositeNode $node, bool $decodeFromStream, array &$context): array
- {
- $prepareDataStmts = $decodeFromStream ? [
- new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [
- $this->builder->var('stream'),
- $this->builder->var('offset'),
- $this->builder->var('length'),
- ]))),
- ] : [];
-
- $providersStmts = [];
- $nodesStmts = [];
-
- $nodeCondition = function (DataModelNodeInterface $node, Expr $accessor): Expr {
- $type = $node->getType();
-
- if ($type->isIdentifiedBy(TypeIdentifier::NULL)) {
- return new Identical($this->builder->val(null), $this->builder->var('data'));
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) {
- return new Identical($this->builder->val(true), $this->builder->var('data'));
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) {
- return new Identical($this->builder->val(false), $this->builder->var('data'));
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) {
- return $this->builder->val(true);
- }
-
- if ($type instanceof CollectionType) {
- return $type->isList()
- ? new BooleanAnd($this->builder->funcCall('\is_array', [$this->builder->var('data')]), $this->builder->funcCall('\array_is_list', [$this->builder->var('data')]))
- : $this->builder->funcCall('\is_array', [$this->builder->var('data')]);
- }
-
- while ($type instanceof WrappingTypeInterface) {
- $type = $type->getWrappedType();
- }
-
- if ($type instanceof BackedEnumType) {
- return $this->builder->funcCall('\is_'.$type->getBackingType()->getTypeIdentifier()->value, [$this->builder->var('data')]);
- }
-
- if ($type instanceof ObjectType) {
- return $this->builder->funcCall('\is_array', [$this->builder->var('data')]);
- }
-
- if ($type instanceof BuiltinType) {
- return $this->builder->funcCall('\is_'.$type->getTypeIdentifier()->value, [$this->builder->var('data')]);
- }
-
- throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class));
- };
-
- foreach ($node->getNodes() as $n) {
- if ($this->nodeOnlyNeedsDecode($n, $decodeFromStream)) {
- $nodeValueStmt = $this->buildFormatValueStatement($n, $this->builder->var('data'));
- } else {
- $providersStmts = [...$providersStmts, ...$this->buildProvidersStatements($n, $decodeFromStream, $context)];
- $nodeValueStmt = $this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($n->getIdentifier())),
- [$this->builder->var('data')],
- );
- }
-
- $nodesStmts[] = new If_($nodeCondition($n, $this->builder->var('data')), ['stmts' => [new Return_($nodeValueStmt)]]);
- }
-
- $params = $decodeFromStream
- ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))]
- : [new Param($this->builder->var('data'))];
-
- return [
- ...$providersStmts,
- new Expression(new Assign(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())),
- new Closure([
- 'static' => true,
- 'params' => $params,
- 'uses' => [
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('instantiator')),
- new ClosureUse($this->builder->var('providers'), byRef: true),
- ],
- 'stmts' => [
- ...$prepareDataStmts,
- ...$nodesStmts,
- new Expression(new Throw_($this->builder->new(new FullyQualified(UnexpectedValueException::class), [$this->builder->funcCall('\sprintf', [
- $this->builder->val(\sprintf('Unexpected "%%s" value for "%s".', $node->getIdentifier())),
- $this->builder->funcCall('\get_debug_type', [$this->builder->var('data')]),
- ])]))),
- ],
- ]),
- )),
- ];
- }
-
- /**
- * @param array $context
- *
- * @return list
- */
- private function buildCollectionNodeStatements(CollectionNode $node, bool $decodeFromStream, array &$context): array
- {
- if ($decodeFromStream) {
- $itemValueStmt = $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream)
- ? $this->buildFormatValueStatement(
- $node->getItemNode(),
- $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [
- $this->builder->var('stream'),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)),
- ]),
- )
- : $this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getItemNode()->getIdentifier())), [
- $this->builder->var('stream'),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)),
- ],
- );
- } else {
- $itemValueStmt = $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream)
- ? $this->builder->var('v')
- : $this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getItemNode()->getIdentifier())),
- [$this->builder->var('v')],
- );
- }
-
- $iterableClosureParams = $decodeFromStream
- ? [new Param($this->builder->var('stream')), new Param($this->builder->var('data'))]
- : [new Param($this->builder->var('data'))];
-
- $iterableClosureStmts = [
- new Expression(new Assign(
- $this->builder->var('iterable'),
- new Closure([
- 'static' => true,
- 'params' => $iterableClosureParams,
- 'uses' => [
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('instantiator')),
- new ClosureUse($this->builder->var('providers'), byRef: true),
- ],
- 'stmts' => [
- new Foreach_($this->builder->var('data'), $this->builder->var('v'), [
- 'keyVar' => $this->builder->var('k'),
- 'stmts' => [new Expression(new Yield_($itemValueStmt, $this->builder->var('k')))],
- ]),
- ],
- ]),
- )),
- ];
-
- $iterableValueStmt = $decodeFromStream
- ? $this->builder->funcCall($this->builder->var('iterable'), [$this->builder->var('stream'), $this->builder->var('data')])
- : $this->builder->funcCall($this->builder->var('iterable'), [$this->builder->var('data')]);
-
- $prepareDataStmts = $decodeFromStream ? [
- new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall(
- new FullyQualified(Splitter::class),
- $node->getType()->isList() ? 'splitList' : 'splitDict',
- [$this->builder->var('stream'), $this->builder->var('offset'), $this->builder->var('length')],
- ))),
- ] : [];
-
- $params = $decodeFromStream
- ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))]
- : [new Param($this->builder->var('data'))];
-
- return [
- new Expression(new Assign(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())),
- new Closure([
- 'static' => true,
- 'params' => $params,
- 'uses' => [
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('instantiator')),
- new ClosureUse($this->builder->var('providers'), byRef: true),
- ],
- 'stmts' => [
- ...$prepareDataStmts,
- ...$iterableClosureStmts,
- new Return_($node->getType()->isIdentifiedBy(TypeIdentifier::ARRAY) ? $this->builder->funcCall('\iterator_to_array', [$iterableValueStmt]) : $iterableValueStmt),
- ],
- ]),
- )),
- ...($this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream) ? [] : $this->buildProvidersStatements($node->getItemNode(), $decodeFromStream, $context)),
- ];
- }
-
- /**
- * @param array $context
- *
- * @return list
- */
- private function buildObjectNodeStatements(ObjectNode $node, bool $decodeFromStream, array &$context): array
- {
- if ($node->isMock()) {
- return [];
- }
-
- $propertyValueProvidersStmts = [];
- $stringPropertiesValuesStmts = [];
- $streamPropertiesValuesStmts = [];
-
- foreach ($node->getProperties() as $streamedName => $property) {
- $propertyValueProvidersStmts = [
- ...$propertyValueProvidersStmts,
- ...($this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream) ? [] : $this->buildProvidersStatements($property['value'], $decodeFromStream, $context)),
- ];
-
- if ($decodeFromStream) {
- $propertyValueStmt = $this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream)
- ? $this->buildFormatValueStatement(
- $property['value'],
- $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [
- $this->builder->var('stream'),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)),
- ]),
- )
- : $this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($property['value']->getIdentifier())), [
- $this->builder->var('stream'),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)),
- new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)),
- ],
- );
-
- $streamPropertiesValuesStmts[] = new MatchArm([$this->builder->val($streamedName)], new Assign(
- $this->builder->propertyFetch($this->builder->var('object'), $property['name']),
- $property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr(),
- ));
- } else {
- $propertyValueStmt = $this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream)
- ? new Coalesce(new ArrayDimFetch($this->builder->var('data'), $this->builder->val($streamedName)), $this->builder->val('_symfony_missing_value'))
- : new Ternary(
- $this->builder->funcCall('\array_key_exists', [$this->builder->val($streamedName), $this->builder->var('data')]),
- $this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($property['value']->getIdentifier())),
- [new ArrayDimFetch($this->builder->var('data'), $this->builder->val($streamedName))],
- ),
- $this->builder->val('_symfony_missing_value'),
- );
-
- $stringPropertiesValuesStmts[] = new ArrayItem(
- $property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr(),
- $this->builder->val($property['name']),
- );
- }
- }
-
- $params = $decodeFromStream
- ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))]
- : [new Param($this->builder->var('data'))];
-
- $prepareDataStmts = $decodeFromStream ? [
- new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall(
- new FullyQualified(Splitter::class),
- 'splitDict',
- [$this->builder->var('stream'), $this->builder->var('offset'), $this->builder->var('length')],
- ))),
- ] : [];
-
- if ($decodeFromStream) {
- $instantiateStmts = [
- new Return_($this->builder->methodCall($this->builder->var('instantiator'), 'instantiate', [
- new ClassConstFetch(new FullyQualified($node->getType()->getClassName()), 'class'),
- new Closure([
- 'static' => true,
- 'params' => [new Param($this->builder->var('object'))],
- 'uses' => [
- new ClosureUse($this->builder->var('stream')),
- new ClosureUse($this->builder->var('data')),
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('instantiator')),
- new ClosureUse($this->builder->var('providers'), byRef: true),
- ],
- 'stmts' => [
- new Foreach_($this->builder->var('data'), $this->builder->var('v'), [
- 'keyVar' => $this->builder->var('k'),
- 'stmts' => [new Expression(new Match_(
- $this->builder->var('k'),
- [...$streamPropertiesValuesStmts, new MatchArm(null, $this->builder->val(null))],
- ))],
- ]),
- ],
- ]),
- ])),
- ];
- } else {
- $instantiateStmts = [
- new Return_($this->builder->methodCall($this->builder->var('instantiator'), 'instantiate', [
- new ClassConstFetch(new FullyQualified($node->getType()->getClassName()), 'class'),
- $this->builder->funcCall('\array_filter', [
- new Array_($stringPropertiesValuesStmts, ['kind' => Array_::KIND_SHORT]),
- new Closure([
- 'static' => true,
- 'params' => [new Param($this->builder->var('v'))],
- 'stmts' => [new Return_(new NotIdentical($this->builder->val('_symfony_missing_value'), $this->builder->var('v')))],
- ]),
- ]),
- ])),
- ];
- }
-
- return [
- new Expression(new Assign(
- new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())),
- new Closure([
- 'static' => true,
- 'params' => $params,
- 'uses' => [
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('instantiator')),
- new ClosureUse($this->builder->var('providers'), byRef: true),
- ],
- 'stmts' => [
- ...$prepareDataStmts,
- ...$instantiateStmts,
- ],
- ]),
- )),
- ...$propertyValueProvidersStmts,
- ];
- }
-
- private function nodeOnlyNeedsDecode(DataModelNodeInterface $node, bool $decodeFromStream): bool
- {
- if ($node instanceof CompositeNode) {
- foreach ($node->getNodes() as $n) {
- if (!$this->nodeOnlyNeedsDecode($n, $decodeFromStream)) {
- return false;
- }
- }
-
- return true;
- }
-
- if ($node instanceof CollectionNode) {
- if ($decodeFromStream) {
- return false;
- }
-
- return $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream);
- }
-
- if ($node instanceof ObjectNode) {
- return false;
- }
-
- if ($node instanceof BackedEnumNode) {
- return false;
- }
-
- if ($node instanceof ScalarNode) {
- return !$node->getType()->isIdentifiedBy(TypeIdentifier::OBJECT);
- }
-
- return true;
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php
new file mode 100644
index 0000000000000..399030226da6a
--- /dev/null
+++ b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php
@@ -0,0 +1,343 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\JsonStreamer\Read;
+
+use Psr\Container\ContainerInterface;
+use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
+use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode;
+use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode;
+use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface;
+use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode;
+use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode;
+use Symfony\Component\JsonStreamer\Exception\LogicException;
+use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Generates PHP code that reads JSON stream.
+ *
+ * @author Mathias Arlaud
+ *
+ * @internal
+ */
+final class PhpGenerator
+{
+ /**
+ * @param array $options
+ * @param array $context
+ */
+ public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStream, array $options = [], array $context = []): string
+ {
+ $context['indentation_level'] = 1;
+
+ $providers = $this->generateProviders($dataModel, $decodeFromStream, $context);
+
+ $context['indentation_level'] = 0;
+
+ if ($decodeFromStream) {
+ return $this->line('line('', $context)
+ .$this->line('/**', $context)
+ .$this->line(' * @return '.$dataModel->getType(), $context)
+ .$this->line(' */', $context)
+ .$this->line('return static function (mixed $stream, \\'.ContainerInterface::class.' $valueTransformers, \\'.LazyInstantiator::class.' $instantiator, array $options): mixed {', $context)
+ .$providers
+ .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream)
+ ? $this->line(' return \\'.Decoder::class.'::decodeStream($stream, 0, null);', $context)
+ : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\']($stream, 0, null);', $context))
+ .$this->line('};', $context);
+ }
+
+ return $this->line('line('', $context)
+ .$this->line('/**', $context)
+ .$this->line(' * @return '.$dataModel->getType(), $context)
+ .$this->line(' */', $context)
+ .$this->line('return static function (string|\\Stringable $string, \\'.ContainerInterface::class.' $valueTransformers, \\'.Instantiator::class.' $instantiator, array $options): mixed {', $context)
+ .$providers
+ .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream)
+ ? $this->line(' return \\'.Decoder::class.'::decodeString((string) $string);', $context)
+ : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\'](\\'.Decoder::class.'::decodeString((string) $string));', $context))
+ .$this->line('};', $context);
+ }
+
+ /**
+ * @param array $context
+ */
+ private function generateProviders(DataModelNodeInterface $node, bool $decodeFromStream, array $context): string
+ {
+ if ($context['providers'][$node->getIdentifier()] ?? false) {
+ return '';
+ }
+
+ $context['providers'][$node->getIdentifier()] = true;
+
+ if ($this->canBeDecodedWithJsonDecode($node, $decodeFromStream)) {
+ return '';
+ }
+
+ if ($node instanceof ScalarNode || $node instanceof BackedEnumNode) {
+ $accessor = $decodeFromStream ? '\\'.Decoder::class.'::decodeStream($stream, $offset, $length)' : '$data';
+ $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';
+
+ return $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) {", $context)
+ .$this->line(' return '.$this->generateValueFormat($node, $accessor).';', $context)
+ .$this->line('};', $context);
+ }
+
+ if ($node instanceof CompositeNode) {
+ $php = '';
+ foreach ($node->getNodes() as $n) {
+ if (!$this->canBeDecodedWithJsonDecode($n, $decodeFromStream)) {
+ $php .= $this->generateProviders($n, $decodeFromStream, $context);
+ }
+ }
+
+ $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';
+
+ $php .= $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context);
+
+ ++$context['indentation_level'];
+
+ $php .= $decodeFromStream ? $this->line('$data = \\'.Decoder::class.'::decodeStream($stream, $offset, $length);', $context) : '';
+
+ foreach ($node->getNodes() as $n) {
+ $value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers[\''.$n->getIdentifier().'\']($data)';
+ $php .= $this->line('if ('.$this->generateCompositeNodeItemCondition($n, '$data').') {', $context)
+ .$this->line(" return $value;", $context)
+ .$this->line('}', $context);
+ }
+
+ $php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "'.$node->getIdentifier().'".\', \\get_debug_type($data)));', $context);
+
+ --$context['indentation_level'];
+
+ return $php.$this->line('};', $context);
+ }
+
+ if ($node instanceof CollectionNode) {
+ $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';
+
+ $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context);
+
+ ++$context['indentation_level'];
+
+ $arguments = $decodeFromStream ? '$stream, $data' : '$data';
+ $php .= ($decodeFromStream ? $this->line('$data = \\'.Splitter::class.'::'.($node->getType()->isList() ? 'splitList' : 'splitDict').'($stream, $offset, $length);', $context) : '')
+ .$this->line("\$iterable = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context)
+ .$this->line(' foreach ($data as $k => $v) {', $context);
+
+ if ($decodeFromStream) {
+ $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)
+ ? $this->line(' yield $k => '.$this->generateValueFormat($node->getItemNode(), '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1]);'), $context)
+ : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($stream, $v[0], $v[1]);', $context);
+ } else {
+ $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)
+ ? $this->line(' yield $k => $v;', $context)
+ : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($v);', $context);
+ }
+
+ $php .= $this->line(' }', $context)
+ .$this->line('};', $context)
+ .$this->line('return '.($node->getType()->isIdentifiedBy(TypeIdentifier::ARRAY) ? "\\iterator_to_array(\$iterable($arguments))" : "\$iterable($arguments)").';', $context);
+
+ --$context['indentation_level'];
+
+ $php .= $this->line('};', $context);
+
+ if (!$this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)) {
+ $php .= $this->generateProviders($node->getItemNode(), $decodeFromStream, $context);
+ }
+
+ return $php;
+ }
+
+ if ($node instanceof ObjectNode) {
+ if ($node->isMock()) {
+ return '';
+ }
+
+ $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';
+
+ $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context);
+
+ ++$context['indentation_level'];
+
+ $php .= $decodeFromStream ? $this->line('$data = \\'.Splitter::class.'::splitDict($stream, $offset, $length);', $context) : '';
+
+ if ($decodeFromStream) {
+ $php .= $this->line('return $instantiator->instantiate(\\'.$node->getType()->getClassName().'::class, static function ($object) use ($stream, $data, $options, $valueTransformers, $instantiator, &$providers) {', $context)
+ .$this->line(' foreach ($data as $k => $v) {', $context)
+ .$this->line(' match ($k) {', $context);
+
+ foreach ($node->getProperties() as $streamedName => $property) {
+ $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)
+ ? $this->generateValueFormat($property['value'], '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1])')
+ : '$providers[\''.$property['value']->getIdentifier().'\']($stream, $v[0], $v[1])';
+
+ $php .= $this->line(" '$streamedName' => \$object->".$property['name'].' = '.$property['accessor']($propertyValuePhp).',', $context);
+ }
+
+ $php .= $this->line(' default => null,', $context)
+ .$this->line(' };', $context)
+ .$this->line(' }', $context)
+ .$this->line('});', $context);
+ } else {
+ $propertiesValuePhp = '[';
+ $separator = '';
+ foreach ($node->getProperties() as $streamedName => $property) {
+ $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)
+ ? "\$data['$streamedName'] ?? '_symfony_missing_value'"
+ : "\\array_key_exists('$streamedName', \$data) ? \$providers['".$property['value']->getIdentifier()."'](\$data['$streamedName']) : '_symfony_missing_value'";
+ $propertiesValuePhp .= "$separator'".$property['name']."' => ".$property['accessor']($propertyValuePhp);
+ $separator = ', ';
+ }
+ $propertiesValuePhp .= ']';
+
+ $php .= $this->line('return $instantiator->instantiate(\\'.$node->getType()->getClassName()."::class, \\array_filter($propertiesValuePhp, static function (\$v) {", $context)
+ .$this->line(' return \'_symfony_missing_value\' !== $v;', $context)
+ .$this->line('}));', $context);
+ }
+
+ --$context['indentation_level'];
+
+ $php .= $this->line('};', $context);
+
+ foreach ($node->getProperties() as $streamedName => $property) {
+ if (!$this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)) {
+ $php .= $this->generateProviders($property['value'], $decodeFromStream, $context);
+ }
+ }
+
+ return $php;
+ }
+
+ throw new LogicException(\sprintf('Unexpected "%s" data model node.', $node::class));
+ }
+
+ private function generateValueFormat(DataModelNodeInterface $node, string $accessor): string
+ {
+ if ($node instanceof BackedEnumNode) {
+ /** @var ObjectType $type */
+ $type = $node->getType();
+
+ return '\\'.$type->getClassName()."::from($accessor)";
+ }
+
+ if ($node instanceof ScalarNode) {
+ /** @var BuiltinType $type */
+ $type = $node->getType();
+
+ return match (true) {
+ TypeIdentifier::NULL === $type->getTypeIdentifier() => 'null',
+ TypeIdentifier::OBJECT === $type->getTypeIdentifier() => "(object) $accessor",
+ default => $accessor,
+ };
+ }
+
+ return $accessor;
+ }
+
+ private function generateCompositeNodeItemCondition(DataModelNodeInterface $node, string $accessor): string
+ {
+ $type = $node->getType();
+
+ if ($type->isIdentifiedBy(TypeIdentifier::NULL)) {
+ return "null === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) {
+ return "true === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) {
+ return "false === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) {
+ return 'true';
+ }
+
+ if ($type instanceof CollectionType) {
+ return $type->isList() ? "\\is_array($accessor) && \\array_is_list($accessor)" : "\\is_array($accessor)";
+ }
+
+ while ($type instanceof WrappingTypeInterface) {
+ $type = $type->getWrappedType();
+ }
+
+ if ($type instanceof BackedEnumType) {
+ return '\\is_'.$type->getBackingType()->getTypeIdentifier()->value."($accessor)";
+ }
+
+ if ($type instanceof ObjectType) {
+ return "\\is_array($accessor)";
+ }
+
+ if ($type instanceof BuiltinType) {
+ return '\\is_'.$type->getTypeIdentifier()->value."($accessor)";
+ }
+
+ throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class));
+ }
+
+ /**
+ * @param array $context
+ */
+ private function line(string $line, array $context): string
+ {
+ return str_repeat(' ', $context['indentation_level']).$line."\n";
+ }
+
+ /**
+ * Determines if the $node can be decoded using a simple "json_decode".
+ */
+ private function canBeDecodedWithJsonDecode(DataModelNodeInterface $node, bool $decodeFromStream): bool
+ {
+ if ($node instanceof CompositeNode) {
+ foreach ($node->getNodes() as $n) {
+ if (!$this->canBeDecodedWithJsonDecode($n, $decodeFromStream)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if ($node instanceof CollectionNode) {
+ if ($decodeFromStream) {
+ return false;
+ }
+
+ return $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream);
+ }
+
+ if ($node instanceof ObjectNode) {
+ return false;
+ }
+
+ if ($node instanceof BackedEnumNode) {
+ return false;
+ }
+
+ if ($node instanceof ScalarNode) {
+ return !$node->getType()->isIdentifiedBy(TypeIdentifier::OBJECT);
+ }
+
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php b/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php
index 18720297b16c6..8f4dc27685351 100644
--- a/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php
+++ b/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php
@@ -11,21 +11,14 @@
namespace Symfony\Component\JsonStreamer\Read;
-use PhpParser\PhpVersion;
-use PhpParser\PrettyPrinter;
-use PhpParser\PrettyPrinter\Standard;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
-use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode;
use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode;
use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface;
use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode;
use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode;
-use Symfony\Component\JsonStreamer\DataModel\ScalarDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor;
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
use Symfony\Component\JsonStreamer\Exception\UnsupportedException;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
@@ -47,8 +40,7 @@
*/
final class StreamReaderGenerator
{
- private ?PhpAstBuilder $phpAstBuilder = null;
- private ?PrettyPrinter $phpPrinter = null;
+ private ?PhpGenerator $phpGenerator = null;
private ?Filesystem $fs = null;
public function __construct(
@@ -69,13 +61,11 @@ public function generate(Type $type, bool $decodeFromStream, array $options = []
return $path;
}
- $this->phpAstBuilder ??= new PhpAstBuilder();
- $this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
+ $this->phpGenerator ??= new PhpGenerator();
$this->fs ??= new Filesystem();
$dataModel = $this->createDataModel($type, $options);
- $nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);
- $content = $this->phpPrinter->prettyPrintFile($nodes)."\n";
+ $php = $this->phpGenerator->generate($dataModel, $decodeFromStream, $options);
if (!$this->fs->exists($this->streamReadersDir)) {
$this->fs->mkdir($this->streamReadersDir);
@@ -84,7 +74,7 @@ public function generate(Type $type, bool $decodeFromStream, array $options = []
$tmpFile = $this->fs->tempnam(\dirname($path), basename($path));
try {
- $this->fs->dumpFile($tmpFile, $content);
+ $this->fs->dumpFile($tmpFile, $php);
$this->fs->rename($tmpFile, $path);
$this->fs->chmod($path, 0666 & ~umask());
} catch (IOException $e) {
@@ -103,7 +93,7 @@ private function getPath(Type $type, bool $decodeFromStream): string
* @param array $options
* @param array $context
*/
- public function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
+ private function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
{
$context['original_type'] ??= $type;
@@ -140,11 +130,10 @@ public function createDataModel(Type $type, array $options = [], array $context
$propertiesNodes[$streamedName] = [
'name' => $propertyMetadata->getName(),
'value' => $this->createDataModel($propertyMetadata->getType(), $options, $context),
- 'accessor' => function (DataAccessorInterface $accessor) use ($propertyMetadata): DataAccessorInterface {
+ 'accessor' => function (string $accessor) use ($propertyMetadata): string {
foreach ($propertyMetadata->getStreamToNativeValueTransformers() as $valueTransformer) {
if (\is_string($valueTransformer)) {
- $valueTransformerServiceAccessor = new FunctionDataAccessor('get', [new ScalarDataAccessor($valueTransformer)], new VariableDataAccessor('valueTransformers'));
- $accessor = new FunctionDataAccessor('transform', [$accessor, new VariableDataAccessor('options')], $valueTransformerServiceAccessor);
+ $accessor = "\$valueTransformers->get('$valueTransformer')->transform($accessor, \$options)";
continue;
}
@@ -158,9 +147,9 @@ public function createDataModel(Type $type, array $options = [], array $context
$functionName = !$functionReflection->getClosureCalledClass()
? $functionReflection->getName()
: \sprintf('%s::%s', $functionReflection->getClosureCalledClass()->getName(), $functionReflection->getName());
- $arguments = $functionReflection->isUserDefined() ? [$accessor, new VariableDataAccessor('options')] : [$accessor];
+ $arguments = $functionReflection->isUserDefined() ? "$accessor, \$options" : $accessor;
- $accessor = new FunctionDataAccessor($functionName, $arguments);
+ $accessor = "$functionName($arguments)";
}
return $accessor;
diff --git a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php
index a7ef7df343d6f..fb57df19ff044 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\JsonStreamer\Tests\DataModel\Write;
use PHPUnit\Framework\TestCase;
-use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor;
use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode;
use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode;
use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode;
@@ -27,7 +26,7 @@ public function testCannotCreateWithOnlyOneType()
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(\sprintf('"%s" expects at least 2 nodes.', CompositeNode::class));
- new CompositeNode(new VariableDataAccessor('data'), [new ScalarNode(new VariableDataAccessor('data'), Type::int())]);
+ new CompositeNode('$data', [new ScalarNode('$data', Type::int())]);
}
public function testCannotCreateWithCompositeNodeParts()
@@ -35,21 +34,21 @@ public function testCannotCreateWithCompositeNodeParts()
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(\sprintf('Cannot set "%s" as a "%s" node.', CompositeNode::class, CompositeNode::class));
- new CompositeNode(new VariableDataAccessor('data'), [
- new CompositeNode(new VariableDataAccessor('data'), [
- new ScalarNode(new VariableDataAccessor('data'), Type::int()),
- new ScalarNode(new VariableDataAccessor('data'), Type::int()),
+ new CompositeNode('$data', [
+ new CompositeNode('$data', [
+ new ScalarNode('$data', Type::int()),
+ new ScalarNode('$data', Type::int()),
]),
- new ScalarNode(new VariableDataAccessor('data'), Type::int()),
+ new ScalarNode('$data', Type::int()),
]);
}
public function testSortNodesOnCreation()
{
- $composite = new CompositeNode(new VariableDataAccessor('data'), [
- $scalar = new ScalarNode(new VariableDataAccessor('data'), Type::int()),
- $object = new ObjectNode(new VariableDataAccessor('data'), Type::object(self::class), []),
- $collection = new CollectionNode(new VariableDataAccessor('data'), Type::list(), new ScalarNode(new VariableDataAccessor('data'), Type::int())),
+ $composite = new CompositeNode('$data', [
+ $scalar = new ScalarNode('$data', Type::int()),
+ $object = new ObjectNode('$data', Type::object(self::class), []),
+ $collection = new CollectionNode('$data', Type::list(), new ScalarNode('$data', Type::int())),
]);
$this->assertSame([$collection, $object, $scalar], $composite->getNodes());
@@ -57,14 +56,14 @@ public function testSortNodesOnCreation()
public function testWithAccessor()
{
- $composite = new CompositeNode(new VariableDataAccessor('data'), [
- new ScalarNode(new VariableDataAccessor('foo'), Type::int()),
- new ScalarNode(new VariableDataAccessor('bar'), Type::int()),
+ $composite = new CompositeNode('$data', [
+ new ScalarNode('$foo', Type::int()),
+ new ScalarNode('$bar', Type::int()),
]);
- $composite = $composite->withAccessor($newAccessor = new VariableDataAccessor('baz'));
+ $composite = $composite->withAccessor('$baz');
foreach ($composite->getNodes() as $node) {
- $this->assertSame($newAccessor, $node->getAccessor());
+ $this->assertSame('$baz', $node->getAccessor());
}
}
}
diff --git a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php
index 0667f731e3d9f..cdc6bf71f4a15 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php
@@ -12,9 +12,6 @@
namespace Symfony\Component\JsonStreamer\Tests\DataModel\Write;
use PHPUnit\Framework\TestCase;
-use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor;
use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode;
use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode;
use Symfony\Component\TypeInfo\Type;
@@ -23,18 +20,18 @@ class ObjectNodeTest extends TestCase
{
public function testWithAccessor()
{
- $object = new ObjectNode(new VariableDataAccessor('foo'), Type::object(self::class), [
- new ScalarNode(new PropertyDataAccessor(new VariableDataAccessor('foo'), 'property'), Type::int()),
- new ScalarNode(new FunctionDataAccessor('function', [], new VariableDataAccessor('foo')), Type::int()),
- new ScalarNode(new FunctionDataAccessor('function', []), Type::int()),
- new ScalarNode(new VariableDataAccessor('bar'), Type::int()),
+ $object = new ObjectNode('$foo', Type::object(self::class), [
+ new ScalarNode('$foo->property', Type::int()),
+ new ScalarNode('$foo->method()', Type::int()),
+ new ScalarNode('function()', Type::int()),
+ new ScalarNode('$bar', Type::int()),
]);
- $object = $object->withAccessor($newAccessor = new VariableDataAccessor('baz'));
+ $object = $object->withAccessor('$baz');
- $this->assertSame($newAccessor, $object->getAccessor());
- $this->assertSame($newAccessor, $object->getProperties()[0]->getAccessor()->getObjectAccessor());
- $this->assertSame($newAccessor, $object->getProperties()[1]->getAccessor()->getObjectAccessor());
- $this->assertNull($object->getProperties()[2]->getAccessor()->getObjectAccessor());
- $this->assertNotSame($newAccessor, $object->getProperties()[3]->getAccessor());
+ $this->assertSame('$baz', $object->getAccessor());
+ $this->assertSame('$baz->property', $object->getProperties()[0]->getAccessor());
+ $this->assertSame('$baz->method()', $object->getProperties()[1]->getAccessor());
+ $this->assertSame('function()', $object->getProperties()[2]->getAccessor());
+ $this->assertSame('$bar', $object->getProperties()[3]->getAccessor());
}
}
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/backed_enum.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/backed_enum.php
index 6c994dd39fbed..2395fea69823f 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/backed_enum.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/backed_enum.php
@@ -1,5 +1,8 @@
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
return \Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string);
};
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/dict.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/dict.stream.php
index 36729b8cec658..183b77955ddd9 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/dict.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/dict.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitDict($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/iterable.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/iterable.php
index a6fedcbd99ba0..45458cd2df0cb 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/iterable.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/iterable.php
@@ -1,5 +1,8 @@
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
return \Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string);
};
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/list.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/list.stream.php
index 2fa9a0a668dbd..35c1d921aeae5 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/list.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/list.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitList($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/mixed.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/mixed.php
index a6fedcbd99ba0..0d68447374ff6 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/mixed.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/mixed.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy::class, \array_filter(['id' => $data['id'] ?? '_symfony_missing_value', 'name' => $data['name'] ?? '_symfony_missing_value'], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php
index b6af2cc29630a..ee8a34a2f8b8a 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php
@@ -1,5 +1,8 @@
|null
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php
index fe3be40f02c7e..93addc49d5b29 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php
@@ -1,5 +1,8 @@
|null
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitDict($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php
index 031d3dc609fac..1213ee6600297 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php
@@ -1,5 +1,8 @@
|null
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php
index 558e1eac1c4e1..717d645bfb8e0 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php
@@ -1,5 +1,8 @@
|null
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitList($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.php
index 4bfffaea57b8c..e7fbe5f057954 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy::class, \array_filter(['id' => $data['id'] ?? '_symfony_missing_value', 'name' => $data['name'] ?? '_symfony_missing_value'], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.stream.php
index 97489cf36f414..afdbe35d9089c 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_dict.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_dict.stream.php
index 0baba407dc54b..cd38d41659421 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_dict.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_dict.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitDict($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.php
index bbba349a3ca93..11efc401589e9 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithOtherDummies::class, \array_filter(['name' => $data['name'] ?? '_symfony_missing_value', 'otherDummyOne' => \array_key_exists('otherDummyOne', $data) ? $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes']($data['otherDummyOne']) : '_symfony_missing_value', 'otherDummyTwo' => \array_key_exists('otherDummyTwo', $data) ? $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy']($data['otherDummyTwo']) : '_symfony_missing_value'], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.stream.php
index df1596179e8e1..1c95a99555fc8 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_in_object.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['iterable'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_iterable.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_iterable.stream.php
index 144749d14959b..9fb08d04a4002 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_iterable.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_iterable.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['iterable'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitDict($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.php
index a243d0c95a76f..84999c8823dae 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.php
@@ -1,5 +1,8 @@
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.stream.php
index 14bb63a2a1dfc..73be0c3639c8a 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_list.stream.php
@@ -1,5 +1,8 @@
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitList($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php
index 647a3aeb923bb..91923525f1d32 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNullableProperties::class, \array_filter(['name' => $data['name'] ?? '_symfony_missing_value', 'enum' => \array_key_exists('enum', $data) ? $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null']($data['enum']) : '_symfony_missing_value'], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php
index 9266447cd53f3..c05e0f05d84cf 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithUnionProperties::class, \array_filter(['value' => \array_key_exists('value', $data) ? $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null|string']($data['value']) : '_symfony_missing_value'], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php
index ef7dc5791c666..1ccf17a7b0bf2 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php
@@ -1,5 +1,8 @@
instantiate(\Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithValueTransformerAttributes::class, \array_filter(['id' => $valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\DivideStringAndCastToIntValueTransformer')->transform($data['id'] ?? '_symfony_missing_value', $options), 'active' => $valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\StringToBooleanValueTransformer')->transform($data['active'] ?? '_symfony_missing_value', $options), 'name' => strtoupper($data['name'] ?? '_symfony_missing_value'), 'range' => Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithValueTransformerAttributes::explodeRange($data['range'] ?? '_symfony_missing_value', $options)], static function ($v) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_value_transformer.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_value_transformer.stream.php
index a6898aeb9bf6e..7904bc2d3a3b6 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_value_transformer.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_value_transformer.stream.php
@@ -1,5 +1,8 @@
|int
+ */
return static function (string|\Stringable $string, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\Instantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
$iterable = static function ($data) use ($options, $valueTransformers, $instantiator, &$providers) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php
index db8d2cffb283e..a5f19897b3dbe 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php
@@ -1,5 +1,8 @@
|int
+ */
return static function (mixed $stream, \Psr\Container\ContainerInterface $valueTransformers, \Symfony\Component\JsonStreamer\Read\LazyInstantiator $instantiator, array $options): mixed {
$providers['array'] = static function ($stream, $offset, $length) use ($options, $valueTransformers, $instantiator, &$providers) {
$data = \Symfony\Component\JsonStreamer\Read\Splitter::splitList($stream, $offset, $length);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/backed_enum.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/backed_enum.php
index cd64125f0a71e..0793dda9f82f2 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/backed_enum.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/backed_enum.php
@@ -1,5 +1,8 @@
value, \JSON_THROW_ON_ERROR, 512);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/bool.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/bool.php
index f645b7c3cc391..79888d618436c 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/bool.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/bool.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield \json_encode($data, \JSON_THROW_ON_ERROR, 512);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/dict.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/dict.php
index cd6e53ba38da1..ca7218ad63810 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/dict.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/dict.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield \json_encode($data, \JSON_THROW_ON_ERROR, 512);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/iterable.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/iterable.php
index cd6e53ba38da1..a0ecc71c74555 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/iterable.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/iterable.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield \json_encode($data, \JSON_THROW_ON_ERROR, 512);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/mixed.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/mixed.php
index cd6e53ba38da1..e121bf57929b0 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/mixed.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/mixed.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield \json_encode($data, \JSON_THROW_ON_ERROR, 512);
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php
index 42f62c6037f05..76ed43bba41f5 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php
@@ -1,5 +1,8 @@
|null $data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
if (\is_array($data)) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php
index f891ae0a649bc..1d06cf77b3e2e 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php
@@ -1,5 +1,8 @@
|null $data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
if (\is_array($data)) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php
index 36499b3d3035c..7fbc49cf96edc 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield '{';
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php
index 3f6dc691cbba9..1e04f6b1d8e6a 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield '{';
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php
index bb4a6a45d0a46..3b691fa350048 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php
@@ -1,5 +1,8 @@
$data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
yield '[';
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php
index bc069637c4e42..cd99dd4630fe7 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php
@@ -1,5 +1,8 @@
= 512) {
diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php
index edb5e5c46fe7c..0043bb1872233 100644
--- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php
+++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php
@@ -1,5 +1,8 @@
|int $data
+ */
return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable {
try {
if (\is_array($data)) {
diff --git a/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php b/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php
deleted file mode 100644
index 289448ba465e8..0000000000000
--- a/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\Write;
-
-use PhpParser\Node;
-use PhpParser\Node\Expr\Yield_;
-use PhpParser\Node\Scalar\String_;
-use PhpParser\Node\Stmt\Expression;
-use PhpParser\NodeVisitor;
-use PhpParser\NodeVisitorAbstract;
-
-/**
- * Merges strings that are yielded consequently
- * to reduce the call instructions amount.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class MergingStringVisitor extends NodeVisitorAbstract
-{
- private string $buffer = '';
-
- public function leaveNode(Node $node): int|Node|array|null
- {
- if (!$this->isMergeableNode($node)) {
- return null;
- }
-
- /** @var Node|null $next */
- $next = $node->getAttribute('next');
-
- if ($next && $this->isMergeableNode($next)) {
- $this->buffer .= $node->expr->value->value;
-
- return NodeVisitor::REMOVE_NODE;
- }
-
- $string = $this->buffer.$node->expr->value->value;
- $this->buffer = '';
-
- return new Expression(new Yield_(new String_($string)));
- }
-
- private function isMergeableNode(Node $node): bool
- {
- return $node instanceof Expression
- && $node->expr instanceof Yield_
- && $node->expr->value instanceof String_;
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php b/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php
deleted file mode 100644
index f0b429b42c8f3..0000000000000
--- a/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php
+++ /dev/null
@@ -1,436 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\Write;
-
-use PhpParser\BuilderFactory;
-use PhpParser\Node\ClosureUse;
-use PhpParser\Node\Expr;
-use PhpParser\Node\Expr\ArrayDimFetch;
-use PhpParser\Node\Expr\Assign;
-use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;
-use PhpParser\Node\Expr\BinaryOp\Identical;
-use PhpParser\Node\Expr\BinaryOp\Plus;
-use PhpParser\Node\Expr\Closure;
-use PhpParser\Node\Expr\Instanceof_;
-use PhpParser\Node\Expr\PropertyFetch;
-use PhpParser\Node\Expr\Ternary;
-use PhpParser\Node\Expr\Throw_;
-use PhpParser\Node\Expr\Yield_;
-use PhpParser\Node\Expr\YieldFrom;
-use PhpParser\Node\Identifier;
-use PhpParser\Node\Name\FullyQualified;
-use PhpParser\Node\Param;
-use PhpParser\Node\Scalar\Encapsed;
-use PhpParser\Node\Scalar\EncapsedStringPart;
-use PhpParser\Node\Stmt;
-use PhpParser\Node\Stmt\Catch_;
-use PhpParser\Node\Stmt\Else_;
-use PhpParser\Node\Stmt\ElseIf_;
-use PhpParser\Node\Stmt\Expression;
-use PhpParser\Node\Stmt\Foreach_;
-use PhpParser\Node\Stmt\If_;
-use PhpParser\Node\Stmt\Return_;
-use PhpParser\Node\Stmt\TryCatch;
-use Psr\Container\ContainerInterface;
-use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode;
-use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode;
-use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode;
-use Symfony\Component\JsonStreamer\DataModel\Write\DataModelNodeInterface;
-use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode;
-use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode;
-use Symfony\Component\JsonStreamer\Exception\LogicException;
-use Symfony\Component\JsonStreamer\Exception\NotEncodableValueException;
-use Symfony\Component\JsonStreamer\Exception\RuntimeException;
-use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException;
-use Symfony\Component\TypeInfo\Type\BuiltinType;
-use Symfony\Component\TypeInfo\Type\ObjectType;
-use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
-use Symfony\Component\TypeInfo\TypeIdentifier;
-
-/**
- * Builds a PHP syntax tree that writes data to JSON stream.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class PhpAstBuilder
-{
- private BuilderFactory $builder;
-
- public function __construct()
- {
- $this->builder = new BuilderFactory();
- }
-
- /**
- * @param array $options
- * @param array $context
- *
- * @return list
- */
- public function build(DataModelNodeInterface $dataModel, array $options = [], array $context = []): array
- {
- $context['depth'] = 0;
-
- $generatorStmts = $this->buildGeneratorStatementsByIdentifiers($dataModel, $options, $context);
-
- // filter generators to mock only
- $generatorStmts = array_merge(...array_values(array_intersect_key($generatorStmts, $context['mocks'] ?? [])));
- $context['generators'] = array_intersect_key($context['generators'] ?? [], $context['mocks'] ?? []);
-
- return [new Return_(new Closure([
- 'static' => true,
- 'params' => [
- new Param($this->builder->var('data'), type: new Identifier('mixed')),
- new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)),
- new Param($this->builder->var('options'), type: new Identifier('array')),
- ],
- 'returnType' => new FullyQualified(\Traversable::class),
- 'stmts' => [
- ...$generatorStmts,
- new TryCatch(
- $this->buildYieldStatements($dataModel, $options, $context),
- [new Catch_([new FullyQualified(\JsonException::class)], $this->builder->var('e'), [
- new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [
- $this->builder->methodCall($this->builder->var('e'), 'getMessage'),
- $this->builder->val(0),
- $this->builder->var('e'),
- ]))),
- ])]
- ),
- ],
- ]))];
- }
-
- /**
- * @param array $options
- * @param array $context
- *
- * @return array>
- */
- private function buildGeneratorStatementsByIdentifiers(DataModelNodeInterface $node, array $options, array &$context): array
- {
- if ($context['generators'][$node->getIdentifier()] ?? false) {
- return [];
- }
-
- if ($node instanceof CollectionNode) {
- return $this->buildGeneratorStatementsByIdentifiers($node->getItemNode(), $options, $context);
- }
-
- if ($node instanceof CompositeNode) {
- $stmts = [];
-
- foreach ($node->getNodes() as $n) {
- $stmts = [
- ...$stmts,
- ...$this->buildGeneratorStatementsByIdentifiers($n, $options, $context),
- ];
- }
-
- return $stmts;
- }
-
- if (!$node instanceof ObjectNode) {
- return [];
- }
-
- if ($node->isMock()) {
- $context['mocks'][$node->getIdentifier()] = true;
-
- return [];
- }
-
- $context['building_generator'] = true;
-
- $stmts = [
- $node->getIdentifier() => [
- new Expression(new Assign(
- new ArrayDimFetch($this->builder->var('generators'), $this->builder->val($node->getIdentifier())),
- new Closure([
- 'static' => true,
- 'params' => [
- new Param($this->builder->var('data')),
- new Param($this->builder->var('depth')),
- ],
- 'uses' => [
- new ClosureUse($this->builder->var('valueTransformers')),
- new ClosureUse($this->builder->var('options')),
- new ClosureUse($this->builder->var('generators'), byRef: true),
- ],
- 'stmts' => [
- new If_(new GreaterOrEqual($this->builder->var('depth'), $this->builder->val(512)), [
- 'stmts' => [new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [$this->builder->val('Maximum stack depth exceeded')])))],
- ]),
- ...$this->buildYieldStatements($node->withAccessor(new VariableDataAccessor('data')), $options, $context),
- ],
- ]),
- )),
- ],
- ];
-
- foreach ($node->getProperties() as $n) {
- $stmts = [
- ...$stmts,
- ...$this->buildGeneratorStatementsByIdentifiers($n, $options, $context),
- ];
- }
-
- unset($context['building_generator']);
- $context['generators'][$node->getIdentifier()] = true;
-
- return $stmts;
- }
-
- /**
- * @param array $options
- * @param array $context
- *
- * @return list
- */
- private function buildYieldStatements(DataModelNodeInterface $dataModelNode, array $options, array $context): array
- {
- $accessor = $dataModelNode->getAccessor()->toPhpExpr();
-
- if ($this->dataModelOnlyNeedsEncode($dataModelNode)) {
- return [
- new Expression(new Yield_($this->encodeValue($accessor, $context))),
- ];
- }
-
- if ($context['depth'] >= 512) {
- return [
- new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [$this->builder->val('Maximum stack depth exceeded')]))),
- ];
- }
-
- if ($dataModelNode instanceof ScalarNode) {
- $scalarAccessor = match (true) {
- TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->builder->val('null'),
- TypeIdentifier::BOOL === $dataModelNode->getType()->getTypeIdentifier() => new Ternary($accessor, $this->builder->val('true'), $this->builder->val('false')),
- default => $this->encodeValue($accessor, $context),
- };
-
- return [
- new Expression(new Yield_($scalarAccessor)),
- ];
- }
-
- if ($dataModelNode instanceof BackedEnumNode) {
- return [
- new Expression(new Yield_($this->encodeValue(new PropertyFetch($accessor, 'value'), $context))),
- ];
- }
-
- if ($dataModelNode instanceof CompositeNode) {
- $nodeCondition = function (DataModelNodeInterface $node): Expr {
- $accessor = $node->getAccessor()->toPhpExpr();
- $type = $node->getType();
-
- if ($type->isIdentifiedBy(TypeIdentifier::NULL, TypeIdentifier::NEVER, TypeIdentifier::VOID)) {
- return new Identical($this->builder->val(null), $accessor);
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) {
- return new Identical($this->builder->val(true), $accessor);
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) {
- return new Identical($this->builder->val(false), $accessor);
- }
-
- if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) {
- return $this->builder->val(true);
- }
-
- while ($type instanceof WrappingTypeInterface) {
- $type = $type->getWrappedType();
- }
-
- if ($type instanceof ObjectType) {
- return new Instanceof_($accessor, new FullyQualified($type->getClassName()));
- }
-
- if ($type instanceof BuiltinType) {
- return $this->builder->funcCall('\is_'.$type->getTypeIdentifier()->value, [$accessor]);
- }
-
- throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class));
- };
-
- $stmtsAndConditions = array_map(fn (DataModelNodeInterface $n): array => [
- 'condition' => $nodeCondition($n),
- 'stmts' => $this->buildYieldStatements($n, $options, $context),
- ], $dataModelNode->getNodes());
-
- $if = $stmtsAndConditions[0];
- unset($stmtsAndConditions[0]);
-
- return [
- new If_($if['condition'], [
- 'stmts' => $if['stmts'],
- 'elseifs' => array_map(fn (array $s): ElseIf_ => new ElseIf_($s['condition'], $s['stmts']), $stmtsAndConditions),
- 'else' => new Else_([
- new Expression(new Throw_($this->builder->new(new FullyQualified(UnexpectedValueException::class), [$this->builder->funcCall('\sprintf', [
- $this->builder->val('Unexpected "%s" value.'),
- $this->builder->funcCall('\get_debug_type', [$accessor]),
- ])]))),
- ]),
- ]),
- ];
- }
-
- if ($dataModelNode instanceof CollectionNode) {
- ++$context['depth'];
-
- if ($dataModelNode->getType()->isList()) {
- return [
- new Expression(new Yield_($this->builder->val('['))),
- new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(''))),
- new Foreach_($accessor, $dataModelNode->getItemNode()->getAccessor()->toPhpExpr(), [
- 'stmts' => [
- new Expression(new Yield_($this->builder->var('prefix'))),
- ...$this->buildYieldStatements($dataModelNode->getItemNode(), $options, $context),
- new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(','))),
- ],
- ]),
- new Expression(new Yield_($this->builder->val(']'))),
- ];
- }
-
- $escapedKey = $dataModelNode->getType()->getCollectionKeyType()->isIdentifiedBy(TypeIdentifier::INT)
- ? new Ternary($this->builder->funcCall('is_int', [$this->builder->var('key')]), $this->builder->var('key'), $this->escapeString($this->builder->var('key')))
- : $this->escapeString($this->builder->var('key'));
-
- return [
- new Expression(new Yield_($this->builder->val('{'))),
- new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(''))),
- new Foreach_($accessor, $dataModelNode->getItemNode()->getAccessor()->toPhpExpr(), [
- 'keyVar' => $this->builder->var('key'),
- 'stmts' => [
- new Expression(new Assign($this->builder->var('key'), $escapedKey)),
- new Expression(new Yield_(new Encapsed([
- $this->builder->var('prefix'),
- new EncapsedStringPart('"'),
- $this->builder->var('key'),
- new EncapsedStringPart('":'),
- ]))),
- ...$this->buildYieldStatements($dataModelNode->getItemNode(), $options, $context),
- new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(','))),
- ],
- ]),
- new Expression(new Yield_($this->builder->val('}'))),
- ];
- }
-
- if ($dataModelNode instanceof ObjectNode) {
- if (isset($context['generators'][$dataModelNode->getIdentifier()]) || $dataModelNode->isMock()) {
- $depthArgument = ($context['building_generator'] ?? false)
- ? new Plus($this->builder->var('depth'), $this->builder->val(1))
- : $this->builder->val($context['depth']);
-
- return [
- new Expression(new YieldFrom($this->builder->funcCall(
- new ArrayDimFetch($this->builder->var('generators'), $this->builder->val($dataModelNode->getIdentifier())),
- [$accessor, $depthArgument],
- ))),
- ];
- }
-
- $objectStmts = [new Expression(new Yield_($this->builder->val('{')))];
- $separator = '';
-
- ++$context['depth'];
-
- foreach ($dataModelNode->getProperties() as $name => $propertyNode) {
- $encodedName = json_encode($name);
- if (false === $encodedName) {
- throw new RuntimeException(\sprintf('Cannot encode "%s"', $name));
- }
-
- $encodedName = substr($encodedName, 1, -1);
-
- $objectStmts = [
- ...$objectStmts,
- new Expression(new Yield_($this->builder->val($separator))),
- new Expression(new Yield_($this->builder->val('"'))),
- new Expression(new Yield_($this->builder->val($encodedName))),
- new Expression(new Yield_($this->builder->val('":'))),
- ...$this->buildYieldStatements($propertyNode, $options, $context),
- ];
-
- $separator = ',';
- }
-
- $objectStmts[] = new Expression(new Yield_($this->builder->val('}')));
-
- return $objectStmts;
- }
-
- throw new LogicException(\sprintf('Unexpected "%s" node', $dataModelNode::class));
- }
-
- /**
- * @param array $context
- */
- private function encodeValue(Expr $value, array $context): Expr
- {
- return $this->builder->funcCall('\json_encode', [
- $value,
- $this->builder->constFetch('\\JSON_THROW_ON_ERROR'),
- $this->builder->val(512 - $context['depth']),
- ]);
- }
-
- private function escapeString(Expr $string): Expr
- {
- return $this->builder->funcCall('\substr', [
- $this->builder->funcCall('\json_encode', [$string]),
- $this->builder->val(1),
- $this->builder->val(-1),
- ]);
- }
-
- private function dataModelOnlyNeedsEncode(DataModelNodeInterface $dataModel, int $depth = 0): bool
- {
- if ($dataModel instanceof CompositeNode) {
- foreach ($dataModel->getNodes() as $node) {
- if (!$this->dataModelOnlyNeedsEncode($node, $depth)) {
- return false;
- }
- }
-
- return true;
- }
-
- if ($dataModel instanceof CollectionNode) {
- return $this->dataModelOnlyNeedsEncode($dataModel->getItemNode(), $depth + 1);
- }
-
- if (!$dataModel instanceof ScalarNode) {
- return false;
- }
-
- $type = $dataModel->getType();
-
- // "null" will be written directly using the "null" string
- // "bool" will be written directly using the "true" or "false" string
- // but it must not prevent any json_encode if nested
- if ($type->isIdentifiedBy(TypeIdentifier::NULL) || $type->isIdentifiedBy(TypeIdentifier::BOOL)) {
- return $depth > 0;
- }
-
- return true;
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php
new file mode 100644
index 0000000000000..f9fb7eb83bd2d
--- /dev/null
+++ b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php
@@ -0,0 +1,391 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\JsonStreamer\Write;
+
+use Psr\Container\ContainerInterface;
+use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode;
+use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode;
+use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode;
+use Symfony\Component\JsonStreamer\DataModel\Write\DataModelNodeInterface;
+use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode;
+use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode;
+use Symfony\Component\JsonStreamer\Exception\LogicException;
+use Symfony\Component\JsonStreamer\Exception\NotEncodableValueException;
+use Symfony\Component\JsonStreamer\Exception\RuntimeException;
+use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Generates PHP code that writes data to JSON stream.
+ *
+ * @author Mathias Arlaud
+ *
+ * @internal
+ */
+final class PhpGenerator
+{
+ private string $yieldBuffer = '';
+
+ /**
+ * @param array $options
+ * @param array $context
+ */
+ public function generate(DataModelNodeInterface $dataModel, array $options = [], array $context = []): string
+ {
+ $context['depth'] = 0;
+ $context['indentation_level'] = 1;
+
+ $generators = $this->generateObjectGenerators($dataModel, $options, $context);
+
+ // filter generators to mock only
+ $generators = array_intersect_key($generators, $context['mocks'] ?? []);
+ $context['generated_generators'] = array_intersect_key($context['generated_generators'] ?? [], $context['mocks'] ?? []);
+
+ $context['indentation_level'] = 2;
+ $yields = $this->generateYields($dataModel, $options, $context)
+ .$this->flushYieldBuffer($context);
+
+ $context['indentation_level'] = 0;
+
+ return $this->line('line('', $context)
+ .$this->line('/**', $context)
+ .$this->line(' * @param '.$dataModel->getType().' $data', $context)
+ .$this->line(' */', $context)
+ .$this->line('return static function (mixed $data, \\'.ContainerInterface::class.' $valueTransformers, array $options): \\Traversable {', $context)
+ .implode('', $generators)
+ .$this->line(' try {', $context)
+ .$yields
+ .$this->line(' } catch (\\JsonException $e) {', $context)
+ .$this->line(' throw new \\'.NotEncodableValueException::class.'($e->getMessage(), 0, $e);', $context)
+ .$this->line(' }', $context)
+ .$this->line('};', $context);
+ }
+
+ /**
+ * @param array $options
+ * @param array $context
+ *
+ * @return array
+ */
+ private function generateObjectGenerators(DataModelNodeInterface $node, array $options, array &$context): array
+ {
+ if ($context['generated_generators'][$node->getIdentifier()] ?? false) {
+ return [];
+ }
+
+ if ($node instanceof CollectionNode) {
+ return $this->generateObjectGenerators($node->getItemNode(), $options, $context);
+ }
+
+ if ($node instanceof CompositeNode) {
+ $generators = [];
+ foreach ($node->getNodes() as $n) {
+ $generators = [
+ ...$generators,
+ ...$this->generateObjectGenerators($n, $options, $context),
+ ];
+ }
+
+ return $generators;
+ }
+
+ if ($node instanceof ObjectNode) {
+ if ($node->isMock()) {
+ $context['mocks'][$node->getIdentifier()] = true;
+
+ return [];
+ }
+
+ $context['generating_generator'] = true;
+
+ ++$context['indentation_level'];
+ $yields = $this->generateYields($node->withAccessor('$data'), $options, $context)
+ .$this->flushYieldBuffer($context);
+ --$context['indentation_level'];
+
+ $generators = [
+ $node->getIdentifier() => $this->line('$generators[\''.$node->getIdentifier().'\'] = static function ($data, $depth) use ($valueTransformers, $options, &$generators) {', $context)
+ .$this->line(' if ($depth >= 512) {', $context)
+ .$this->line(' throw new \\'.NotEncodableValueException::class.'(\'Maximum stack depth exceeded\');', $context)
+ .$this->line(' }', $context)
+ .$yields
+ .$this->line('};', $context),
+ ];
+
+ foreach ($node->getProperties() as $n) {
+ $generators = [
+ ...$generators,
+ ...$this->generateObjectGenerators($n, $options, $context),
+ ];
+ }
+
+ unset($context['generating_generator']);
+ $context['generated_generators'][$node->getIdentifier()] = true;
+
+ return $generators;
+ }
+
+ return [];
+ }
+
+ /**
+ * @param array $options
+ * @param array $context
+ */
+ private function generateYields(DataModelNodeInterface $dataModelNode, array $options, array $context): string
+ {
+ $accessor = $dataModelNode->getAccessor();
+
+ if ($this->canBeEncodedWithJsonEncode($dataModelNode)) {
+ return $this->yield($this->encode($accessor, $context), $context);
+ }
+
+ if ($context['depth'] >= 512) {
+ return $this->line('throw new '.NotEncodableValueException::class.'(\'Maximum stack depth exceeded\');', $context);
+ }
+
+ if ($dataModelNode instanceof ScalarNode) {
+ return match (true) {
+ TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->yieldString('null', $context),
+ TypeIdentifier::BOOL === $dataModelNode->getType()->getTypeIdentifier() => $this->yield("$accessor ? 'true' : 'false'", $context),
+ default => $this->yield($this->encode($accessor, $context), $context),
+ };
+ }
+
+ if ($dataModelNode instanceof BackedEnumNode) {
+ return $this->yield($this->encode("{$accessor}->value", $context), $context);
+ }
+
+ if ($dataModelNode instanceof CompositeNode) {
+ $php = $this->flushYieldBuffer($context);
+ foreach ($dataModelNode->getNodes() as $i => $node) {
+ $php .= $this->line((0 === $i ? 'if' : '} elseif').' ('.$this->generateCompositeNodeItemCondition($node).') {', $context);
+
+ ++$context['indentation_level'];
+ $php .= $this->generateYields($node, $options, $context)
+ .$this->flushYieldBuffer($context);
+ --$context['indentation_level'];
+ }
+
+ return $php
+ .$this->flushYieldBuffer($context)
+ .$this->line('} else {', $context)
+ .$this->line(' throw new \\'.UnexpectedValueException::class."(\\sprintf('Unexpected \"%s\" value.', \get_debug_type($accessor)));", $context)
+ .$this->line('}', $context);
+ }
+
+ if ($dataModelNode instanceof CollectionNode) {
+ ++$context['depth'];
+
+ if ($dataModelNode->getType()->isList()) {
+ $php = $this->yieldString('[', $context)
+ .$this->flushYieldBuffer($context)
+ .$this->line('$prefix = \'\';', $context)
+ .$this->line("foreach ($accessor as ".$dataModelNode->getItemNode()->getAccessor().') {', $context);
+
+ ++$context['indentation_level'];
+ $php .= $this->yield('$prefix', $context)
+ .$this->generateYields($dataModelNode->getItemNode(), $options, $context)
+ .$this->flushYieldBuffer($context)
+ .$this->line('$prefix = \',\';', $context);
+
+ --$context['indentation_level'];
+
+ return $php
+ .$this->line('}', $context)
+ .$this->yieldString(']', $context);
+ }
+
+ $escapedKey = $dataModelNode->getType()->getCollectionKeyType()->isIdentifiedBy(TypeIdentifier::INT)
+ ? '$key = is_int($key) ? $key : \substr(\json_encode($key), 1, -1);'
+ : '$key = \substr(\json_encode($key), 1, -1);';
+
+ $php = $this->yieldString('{', $context)
+ .$this->flushYieldBuffer($context)
+ .$this->line('$prefix = \'\';', $context)
+ .$this->line("foreach ($accessor as \$key => ".$dataModelNode->getItemNode()->getAccessor().') {', $context);
+
+ ++$context['indentation_level'];
+ $php .= $this->line($escapedKey, $context)
+ .$this->yield('"{$prefix}\"{$key}\":"', $context)
+ .$this->generateYields($dataModelNode->getItemNode(), $options, $context)
+ .$this->flushYieldBuffer($context)
+ .$this->line('$prefix = \',\';', $context);
+
+ --$context['indentation_level'];
+
+ return $php
+ .$this->line('}', $context)
+ .$this->yieldString('}', $context);
+ }
+
+ if ($dataModelNode instanceof ObjectNode) {
+ if (isset($context['generated_generators'][$dataModelNode->getIdentifier()]) || $dataModelNode->isMock()) {
+ $depthArgument = ($context['generating_generator'] ?? false) ? '$depth + 1' : (string) $context['depth'];
+
+ return $this->line('yield from $generators[\''.$dataModelNode->getIdentifier().'\']('.$accessor.', '.$depthArgument.');', $context);
+ }
+
+ $php = $this->yieldString('{', $context);
+ $separator = '';
+
+ ++$context['depth'];
+
+ foreach ($dataModelNode->getProperties() as $name => $propertyNode) {
+ $encodedName = json_encode($name);
+ if (false === $encodedName) {
+ throw new RuntimeException(\sprintf('Cannot encode "%s"', $name));
+ }
+
+ $encodedName = substr($encodedName, 1, -1);
+
+ $php .= $this->yieldString($separator, $context)
+ .$this->yieldString('"', $context)
+ .$this->yieldString($encodedName, $context)
+ .$this->yieldString('":', $context)
+ .$this->generateYields($propertyNode, $options, $context);
+
+ $separator = ',';
+ }
+
+ return $php
+ .$this->yieldString('}', $context);
+ }
+
+ throw new LogicException(\sprintf('Unexpected "%s" node', $dataModelNode::class));
+ }
+
+ /**
+ * @param array $context
+ */
+ private function encode(string $value, array $context): string
+ {
+ return "\json_encode($value, \\JSON_THROW_ON_ERROR, ". 512 - $context['depth'].')';
+ }
+
+ /**
+ * @param array $context
+ */
+ private function yield(string $value, array $context): string
+ {
+ return $this->flushYieldBuffer($context)
+ .$this->line("yield $value;", $context);
+ }
+
+ /**
+ * @param array $context
+ */
+ private function yieldString(string $string, array $context): string
+ {
+ $this->yieldBuffer .= $string;
+
+ return '';
+ }
+
+ /**
+ * @param array $context
+ */
+ private function flushYieldBuffer(array $context): string
+ {
+ if ('' === $this->yieldBuffer) {
+ return '';
+ }
+
+ $yieldBuffer = $this->yieldBuffer;
+ $this->yieldBuffer = '';
+
+ return $this->yield("'$yieldBuffer'", $context);
+ }
+
+ private function generateCompositeNodeItemCondition(DataModelNodeInterface $node): string
+ {
+ $accessor = $node->getAccessor();
+ $type = $node->getType();
+
+ if ($type->isIdentifiedBy(TypeIdentifier::NULL, TypeIdentifier::NEVER, TypeIdentifier::VOID)) {
+ return "null === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) {
+ return "true === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) {
+ return "false === $accessor";
+ }
+
+ if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) {
+ return 'true';
+ }
+
+ while ($type instanceof WrappingTypeInterface) {
+ $type = $type->getWrappedType();
+ }
+
+ if ($type instanceof ObjectType) {
+ return "$accessor instanceof \\".$type->getClassName();
+ }
+
+ if ($type instanceof BuiltinType) {
+ return '\\is_'.$type->getTypeIdentifier()->value."($accessor)";
+ }
+
+ throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class));
+ }
+
+ /**
+ * @param array $context
+ */
+ private function line(string $line, array $context): string
+ {
+ return str_repeat(' ', $context['indentation_level']).$line."\n";
+ }
+
+ /**
+ * Determines if the $node can be encoded using a simple "json_encode".
+ */
+ private function canBeEncodedWithJsonEncode(DataModelNodeInterface $node, int $depth = 0): bool
+ {
+ if ($node instanceof CompositeNode) {
+ foreach ($node->getNodes() as $n) {
+ if (!$this->canBeEncodedWithJsonEncode($n, $depth)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if ($node instanceof CollectionNode) {
+ return $this->canBeEncodedWithJsonEncode($node->getItemNode(), $depth + 1);
+ }
+
+ if (!$node instanceof ScalarNode) {
+ return false;
+ }
+
+ $type = $node->getType();
+
+ // "null" will be written directly using the "null" string
+ // "bool" will be written directly using the "true" or "false" string
+ // but it must not prevent any json_encode if nested
+ if ($type->isIdentifiedBy(TypeIdentifier::NULL) || $type->isIdentifiedBy(TypeIdentifier::BOOL)) {
+ return $depth > 0;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php b/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php
deleted file mode 100644
index 4dddaf47aac70..0000000000000
--- a/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php
+++ /dev/null
@@ -1,43 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\JsonStreamer\Write;
-
-use PhpParser\Node;
-use PhpParser\NodeTraverser;
-use PhpParser\NodeVisitor\NodeConnectingVisitor;
-
-/**
- * Optimizes a PHP syntax tree.
- *
- * @author Mathias Arlaud
- *
- * @internal
- */
-final class PhpOptimizer
-{
- /**
- * @param list $nodes
- *
- * @return list
- */
- public function optimize(array $nodes): array
- {
- $traverser = new NodeTraverser();
- $traverser->addVisitor(new NodeConnectingVisitor());
- $nodes = $traverser->traverse($nodes);
-
- $traverser = new NodeTraverser();
- $traverser->addVisitor(new MergingStringVisitor());
-
- return $traverser->traverse($nodes);
- }
-}
diff --git a/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php b/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php
index c437ca0d179f5..4035d84770fbf 100644
--- a/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php
+++ b/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php
@@ -11,16 +11,8 @@
namespace Symfony\Component\JsonStreamer\Write;
-use PhpParser\PhpVersion;
-use PhpParser\PrettyPrinter;
-use PhpParser\PrettyPrinter\Standard;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
-use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\ScalarDataAccessor;
-use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor;
use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode;
use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode;
use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode;
@@ -48,9 +40,7 @@
*/
final class StreamWriterGenerator
{
- private ?PhpAstBuilder $phpAstBuilder = null;
- private ?PhpOptimizer $phpOptimizer = null;
- private ?PrettyPrinter $phpPrinter = null;
+ private ?PhpGenerator $phpGenerator = null;
private ?Filesystem $fs = null;
public function __construct(
@@ -71,17 +61,11 @@ public function generate(Type $type, array $options = []): string
return $path;
}
- $this->phpAstBuilder ??= new PhpAstBuilder();
- $this->phpOptimizer ??= new PhpOptimizer();
- $this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
+ $this->phpGenerator ??= new PhpGenerator();
$this->fs ??= new Filesystem();
- $dataModel = $this->createDataModel($type, new VariableDataAccessor('data'), $options);
-
- $nodes = $this->phpAstBuilder->build($dataModel, $options);
- $nodes = $this->phpOptimizer->optimize($nodes);
-
- $content = $this->phpPrinter->prettyPrintFile($nodes)."\n";
+ $dataModel = $this->createDataModel($type, '$data', $options);
+ $php = $this->phpGenerator->generate($dataModel, $options);
if (!$this->fs->exists($this->streamWritersDir)) {
$this->fs->mkdir($this->streamWritersDir);
@@ -90,7 +74,7 @@ public function generate(Type $type, array $options = []): string
$tmpFile = $this->fs->tempnam(\dirname($path), basename($path));
try {
- $this->fs->dumpFile($tmpFile, $content);
+ $this->fs->dumpFile($tmpFile, $php);
$this->fs->rename($tmpFile, $path);
$this->fs->chmod($path, 0666 & ~umask());
} catch (IOException $e) {
@@ -109,7 +93,7 @@ private function getPath(Type $type): string
* @param array $options
* @param array $context
*/
- private function createDataModel(Type $type, DataAccessorInterface $accessor, array $options = [], array $context = []): DataModelNodeInterface
+ private function createDataModel(Type $type, string $accessor, array $options = [], array $context = []): DataModelNodeInterface
{
$context['original_type'] ??= $type;
@@ -149,12 +133,12 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar
$propertiesNodes = [];
foreach ($propertiesMetadata as $streamedName => $propertyMetadata) {
- $propertyAccessor = new PropertyDataAccessor($accessor, $propertyMetadata->getName());
+ $propertyAccessor = $accessor.'->'.$propertyMetadata->getName();
foreach ($propertyMetadata->getNativeToStreamValueTransformer() as $valueTransformer) {
if (\is_string($valueTransformer)) {
- $valueTransformerServiceAccessor = new FunctionDataAccessor('get', [new ScalarDataAccessor($valueTransformer)], new VariableDataAccessor('valueTransformers'));
- $propertyAccessor = new FunctionDataAccessor('transform', [$propertyAccessor, new VariableDataAccessor('options')], $valueTransformerServiceAccessor);
+ $valueTransformerServiceAccessor = "\$valueTransformers->get('$valueTransformer')";
+ $propertyAccessor = "{$valueTransformerServiceAccessor}->transform($propertyAccessor, \$options)";
continue;
}
@@ -168,9 +152,9 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar
$functionName = !$functionReflection->getClosureCalledClass()
? $functionReflection->getName()
: \sprintf('%s::%s', $functionReflection->getClosureCalledClass()->getName(), $functionReflection->getName());
- $arguments = $functionReflection->isUserDefined() ? [$propertyAccessor, new VariableDataAccessor('options')] : [$propertyAccessor];
+ $arguments = $functionReflection->isUserDefined() ? "$propertyAccessor, \$options" : $propertyAccessor;
- $propertyAccessor = new FunctionDataAccessor($functionName, $arguments);
+ $propertyAccessor = "$functionName($arguments)";
}
$propertiesNodes[$streamedName] = $this->createDataModel($propertyMetadata->getType(), $propertyAccessor, $options, $context);
@@ -183,7 +167,7 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar
return new CollectionNode(
$accessor,
$type,
- $this->createDataModel($type->getCollectionValueType(), new VariableDataAccessor('value'), $options, $context),
+ $this->createDataModel($type->getCollectionValueType(), '$value', $options, $context),
);
}
diff --git a/src/Symfony/Component/JsonStreamer/composer.json b/src/Symfony/Component/JsonStreamer/composer.json
index ba02d9fbc9172..ac3af9ee36b0a 100644
--- a/src/Symfony/Component/JsonStreamer/composer.json
+++ b/src/Symfony/Component/JsonStreamer/composer.json
@@ -16,18 +16,17 @@
}
],
"require": {
- "nikic/php-parser": "^5.3",
"php": ">=8.2",
"psr/container": "^1.1|^2.0",
"psr/log": "^1|^2|^3",
- "symfony/filesystem": "^7.2",
- "symfony/type-info": "^7.2",
- "symfony/var-exporter": "^7.2"
+ "symfony/filesystem": "^7.2|^8.0",
+ "symfony/type-info": "^7.2|^8.0",
+ "symfony/var-exporter": "^7.2|^8.0"
},
"require-dev": {
"phpstan/phpdoc-parser": "^1.0",
- "symfony/dependency-injection": "^7.2",
- "symfony/http-kernel": "^7.2"
+ "symfony/dependency-injection": "^7.2|^8.0",
+ "symfony/http-kernel": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\JsonStreamer\\": "" },
diff --git a/src/Symfony/Component/Ldap/Security/LdapUser.php b/src/Symfony/Component/Ldap/Security/LdapUser.php
index ef73b82422d0b..020fcb5441596 100644
--- a/src/Symfony/Component/Ldap/Security/LdapUser.php
+++ b/src/Symfony/Component/Ldap/Security/LdapUser.php
@@ -47,7 +47,7 @@ public function getRoles(): array
public function getPassword(): ?string
{
- return $this->password;
+ return $this->password ?? null;
}
public function getSalt(): ?string
@@ -89,7 +89,7 @@ public function isEqualTo(UserInterface $user): bool
return false;
}
- if ($this->getPassword() !== $user->getPassword()) {
+ if (($this->getPassword() ?? $user->getPassword()) !== $user->getPassword()) {
return false;
}
diff --git a/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php b/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php
new file mode 100644
index 0000000000000..0a696bcd0c29d
--- /dev/null
+++ b/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Ldap\Tests\Security;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Ldap\Entry;
+use Symfony\Component\Ldap\Security\LdapUser;
+
+class LdapUserTest extends TestCase
+{
+ public function testIsEqualToWorksOnUnserializedUser()
+ {
+ $user = new LdapUser(new Entry('uid=jonhdoe,ou=MyBusiness,dc=symfony,dc=com', []), 'jonhdoe', 'p455w0rd');
+ $unserializedUser = unserialize(serialize($user));
+
+ $this->assertTrue($unserializedUser->isEqualTo($user));
+ }
+}
diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json
index 535ad186207ec..32a9ab27552d7 100644
--- a/src/Symfony/Component/Ldap/composer.json
+++ b/src/Symfony/Component/Ldap/composer.json
@@ -18,11 +18,11 @@
"require": {
"php": ">=8.2",
"ext-ldap": "*",
- "symfony/options-resolver": "^7.3"
+ "symfony/options-resolver": "^7.3|^8.0"
},
"require-dev": {
- "symfony/security-core": "^6.4|^7.0",
- "symfony/security-http": "^6.4|^7.0"
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/security-http": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/security-core": "<6.4"
diff --git a/src/Symfony/Component/Mailer/Bridge/AhaSend/composer.json b/src/Symfony/Component/Mailer/Bridge/AhaSend/composer.json
index 65fae0816c89d..3eeaa278a962d 100644
--- a/src/Symfony/Component/Mailer/Bridge/AhaSend/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/AhaSend/composer.json
@@ -18,11 +18,11 @@
"require": {
"php": ">=8.2",
"psr/event-dispatcher": "^1",
- "symfony/mailer": "^7.3"
+ "symfony/mailer": "^7.3|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\AhaSend\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json
index 3b8cd7cd49cb9..323b03519608e 100644
--- a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json
@@ -18,10 +18,10 @@
"require": {
"php": ">=8.2",
"async-aws/ses": "^1.8",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Amazon\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/composer.json b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json
index c8396c21913e0..2772c273ef38e 100644
--- a/src/Symfony/Component/Mailer/Bridge/Azure/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json
@@ -17,10 +17,10 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Azure\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json b/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json
index 441dada9ef97d..2fa9bfa4905be 100644
--- a/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Brevo/composer.json
@@ -16,12 +16,12 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/mailer": "^7.2"
+ "php": ">=8.2",
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.3|^7.0",
- "symfony/webhook": "^6.3|^7.0"
+ "symfony/http-client": "^6.3|^7.0|^8.0",
+ "symfony/webhook": "^6.3|^7.0|^8.0"
},
"conflict": {
"symfony/mime": "<6.2"
diff --git a/src/Symfony/Component/Mailer/Bridge/Google/composer.json b/src/Symfony/Component/Mailer/Bridge/Google/composer.json
index 13ba43762d942..c60576d8fb9d4 100644
--- a/src/Symfony/Component/Mailer/Bridge/Google/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Google/composer.json
@@ -17,10 +17,10 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Google\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/composer.json b/src/Symfony/Component/Mailer/Bridge/Infobip/composer.json
index e15a7a3d17f4a..5d94ecc9e8c80 100644
--- a/src/Symfony/Component/Mailer/Bridge/Infobip/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Infobip/composer.json
@@ -21,11 +21,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2",
- "symfony/mime": "^6.4|^7.0"
+ "symfony/mailer": "^7.2|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/mime": "<6.4"
diff --git a/src/Symfony/Component/Mailer/Bridge/MailPace/composer.json b/src/Symfony/Component/Mailer/Bridge/MailPace/composer.json
index 9e962e28fc17f..77332cf2cc438 100644
--- a/src/Symfony/Component/Mailer/Bridge/MailPace/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/MailPace/composer.json
@@ -22,10 +22,10 @@
"require": {
"php": ">=8.2",
"psr/event-dispatcher": "^1",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\MailPace\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
index 081b5998e6206..29ffb27b889b1 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^7.2|^8.0"
},
"conflict": {
"symfony/webhook": "<7.2"
diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
index 84e2553a627cc..e5bfb4daddc2e 100644
--- a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
+++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
@@ -22,7 +22,7 @@ final class MailerSendSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, #[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
- parent::__construct('smtp.mailersend.net', 587, true, $dispatcher, $logger);
+ parent::__construct('smtp.mailersend.net', 587, false, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);
diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json
index 96357327da187..23831dc41c80e 100644
--- a/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json
@@ -21,11 +21,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^6.3|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^6.3|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\MailerSend\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
index fa34f9abb7caf..08879782a0bc3 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
@@ -98,9 +98,6 @@ public function testCustomHeader()
$this->assertEquals('amp-html-value', $payload['amp-html']);
}
- /**
- * @legacy
- */
public function testPrefixHeaderWithH()
{
$email = new Email();
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
index 3a5a475e3e44b..b68dbb7152fae 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json
index 3abc7eb31c135..f4877458b212a 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.3"
+ "symfony/mailer": "^7.3|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailjet\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailomat/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailomat/composer.json
index 2d4cc3f1c8515..dd8e043a2a9c2 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailomat/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Mailomat/composer.json
@@ -17,12 +17,12 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/http-foundation": "^7.1",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^7.1|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<7.1"
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json
index 7d448e7c40768..3fa19c63a89ed 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json
@@ -18,11 +18,11 @@
"require": {
"php": ">=8.2",
"psr/event-dispatcher": "^1",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^7.2|^8.0"
},
"conflict": {
"symfony/webhook": "<7.2"
diff --git a/src/Symfony/Component/Mailer/Bridge/Postal/composer.json b/src/Symfony/Component/Mailer/Bridge/Postal/composer.json
index 8c3d3dfe8eda4..62fa6bf19db95 100644
--- a/src/Symfony/Component/Mailer/Bridge/Postal/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Postal/composer.json
@@ -17,10 +17,10 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Postal\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json b/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
index 0451fec7f96ce..45bc2c17b6630 100644
--- a/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
@@ -18,11 +18,11 @@
"require": {
"php": ">=8.2",
"psr/event-dispatcher": "^1",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
diff --git a/src/Symfony/Component/Mailer/Bridge/Resend/composer.json b/src/Symfony/Component/Mailer/Bridge/Resend/composer.json
index 0fe9a6f79df3c..66cdd2efbaa27 100644
--- a/src/Symfony/Component/Mailer/Bridge/Resend/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Resend/composer.json
@@ -16,13 +16,13 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/mailer": "^7.2"
+ "php": ">=8.2",
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/http-foundation": "^7.1",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^7.1|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<7.1"
diff --git a/src/Symfony/Component/Mailer/Bridge/Scaleway/composer.json b/src/Symfony/Component/Mailer/Bridge/Scaleway/composer.json
index 1ad65e470f641..f4c3e825d86d1 100644
--- a/src/Symfony/Component/Mailer/Bridge/Scaleway/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Scaleway/composer.json
@@ -16,11 +16,11 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/mailer": "^7.2"
+ "php": ">=8.2",
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Scaleway\\": "" },
diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json b/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json
index 700aabef20a7d..899b4f6d9d4d0 100644
--- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/mailer": "^7.2"
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/webhook": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^7.2|^8.0"
},
"conflict": {
"symfony/mime": "<6.4",
diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/composer.json b/src/Symfony/Component/Mailer/Bridge/Sweego/composer.json
index 4fbe23334d574..4db381b4a9816 100644
--- a/src/Symfony/Component/Mailer/Bridge/Sweego/composer.json
+++ b/src/Symfony/Component/Mailer/Bridge/Sweego/composer.json
@@ -16,13 +16,13 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/mailer": "^7.2"
+ "php": ">=8.2",
+ "symfony/mailer": "^7.2|^8.0"
},
"require-dev": {
- "symfony/http-client": "^6.4|^7.0",
- "symfony/http-foundation": "^7.1",
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^7.1|^8.0",
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<7.1"
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
index a893441b03a9a..5de88e71fa247 100644
--- a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
+++ b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
@@ -16,6 +16,8 @@
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
@@ -144,6 +146,27 @@ public function testSendOneDeadAndRecoveryWithinRetryPeriod()
$this->assertTransports($t, 1, []);
}
+ public function testSendOneDeadMessageAlterationsDoNotPersist()
+ {
+ $t1 = $this->createMock(TransportInterface::class);
+ $t1->expects($this->once())->method('send')
+ ->willReturnCallback(function (Message $message) {
+ $message->getHeaders()->addTextHeader('X-Transport-1', 'value');
+ throw new TransportException();
+ });
+ $t2 = $this->createMock(TransportInterface::class);
+ $t2->expects($this->once())->method('send');
+ $t = new RoundRobinTransport([$t1, $t2]);
+ $p = new \ReflectionProperty($t, 'cursor');
+ $p->setValue($t, 0);
+ $headers = new Headers();
+ $headers->addTextHeader('X-Shared', 'value');
+ $message = new Message($headers);
+ $t->send($message);
+ $this->assertSame($message->getHeaders()->get('X-Shared')->getBody(), 'value');
+ $this->assertFalse($message->getHeaders()->has('X-Transport-1'));
+ }
+
public function testFailureDebugInformation()
{
$t1 = $this->createMock(TransportInterface::class);
diff --git a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
index 4925b40d0bb6a..e48644f790b56 100644
--- a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
@@ -50,7 +50,7 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess
while ($transport = $this->getNextTransport()) {
try {
- return $transport->send($message, $envelope);
+ return $transport->send(clone $message, $envelope);
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json
index 4336e725133fc..105fa39cd46bb 100644
--- a/src/Symfony/Component/Mailer/composer.json
+++ b/src/Symfony/Component/Mailer/composer.json
@@ -20,15 +20,15 @@
"egulias/email-validator": "^2.1.10|^3|^4",
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/mime": "^7.2",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^7.2|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/console": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/twig-bridge": "^6.4|^7.0"
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/twig-bridge": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-client-contracts": "<2.5",
diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/composer.json b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/composer.json
index f334cd349c969..9e6904978670d 100644
--- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/composer.json
+++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/composer.json
@@ -19,14 +19,14 @@
"php": ">=8.2",
"async-aws/core": "^1.7",
"async-aws/sqs": "^1.0|^2.0",
- "symfony/messenger": "^7.3",
+ "symfony/messenger": "^7.3|^8.0",
"symfony/service-contracts": "^2.5|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"symfony/http-client-contracts": "^2.5|^3",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-client-contracts": "<2.5"
diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json b/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json
index 7e078f524fb9f..fcc2ceba9906e 100644
--- a/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json
+++ b/src/Symfony/Component/Messenger/Bridge/Amqp/composer.json
@@ -18,13 +18,13 @@
"require": {
"php": ">=8.2",
"ext-amqp": "*",
- "symfony/messenger": "^7.3"
+ "symfony/messenger": "^7.3|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" },
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/composer.json b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/composer.json
index bed9817cedab3..a96066c79790b 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/composer.json
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/composer.json
@@ -14,11 +14,11 @@
"require": {
"php": ">=8.2",
"pda/pheanstalk": "^5.1|^7.0",
- "symfony/messenger": "^7.3"
+ "symfony/messenger": "^7.3|^8.0"
},
"require-dev": {
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Beanstalkd\\": "" },
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json b/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json
index eabf0a9138c91..8f98bfc979092 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/composer.json
@@ -18,13 +18,13 @@
"require": {
"php": ">=8.2",
"doctrine/dbal": "^3.6|^4",
- "symfony/messenger": "^7.2",
+ "symfony/messenger": "^7.2|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
"doctrine/persistence": "^1.3|^2|^3",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
},
"conflict": {
"doctrine/persistence": "<1.3"
diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/composer.json b/src/Symfony/Component/Messenger/Bridge/Redis/composer.json
index 050211bb2d36a..d02f4ec0df1be 100644
--- a/src/Symfony/Component/Messenger/Bridge/Redis/composer.json
+++ b/src/Symfony/Component/Messenger/Bridge/Redis/composer.json
@@ -18,11 +18,11 @@
"require": {
"php": ">=8.2",
"ext-redis": "*",
- "symfony/messenger": "^7.3"
+ "symfony/messenger": "^7.3|^8.0"
},
"require-dev": {
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" },
diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
index 1a84381008318..9fcb6a5e17e80 100644
--- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
+++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
@@ -313,7 +313,7 @@ private function convertToBytes(string $memoryLimit): int
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
- $max = (int) $max;
+ $max = (float) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
@@ -326,6 +326,6 @@ private function convertToBytes(string $memoryLimit): int
case 'k': $max *= 1024;
}
- return $max;
+ return (int) $max;
}
}
diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
index 7790e074ad609..8552a64f1a291 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Messenger\Tests\Command;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Tester\CommandCompletionTester;
@@ -59,7 +61,11 @@ public function testBasicRun()
$command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher());
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$tester->execute([
'receivers' => ['dummy-receiver'],
@@ -89,7 +95,11 @@ public function testRunWithBusOption()
$command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher());
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$tester->execute([
'receivers' => ['dummy-receiver'],
@@ -132,7 +142,11 @@ public function testRunWithResetServicesOption(bool $shouldReset)
$command = new ConsumeMessagesCommand($bus, $receiverLocator, new EventDispatcher(), null, [], new ResetServicesListener($servicesResetter));
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$tester->execute(array_merge([
'receivers' => ['dummy-receiver'],
@@ -156,7 +170,11 @@ public function testRunWithInvalidOption(string $option, string $value, string $
$command = new ConsumeMessagesCommand(new RoutableMessageBus(new Container()), $receiverLocator, new EventDispatcher());
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$this->expectException(InvalidOptionException::class);
@@ -194,7 +212,11 @@ public function testRunWithTimeLimit()
$command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher());
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$tester->execute([
'receivers' => ['dummy-receiver'],
@@ -205,6 +227,52 @@ public function testRunWithTimeLimit()
$this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
}
+ public function testRunWithMemoryLimit()
+ {
+ $envelope = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
+
+ $receiver = $this->createMock(ReceiverInterface::class);
+ $receiver->method('get')->willReturn([$envelope]);
+
+ $receiverLocator = new Container();
+ $receiverLocator->set('dummy-receiver', $receiver);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+
+ $busLocator = new Container();
+ $busLocator->set('dummy-bus', $bus);
+
+ $logger = new class() implements LoggerInterface {
+ use LoggerTrait;
+
+ public array $logs = [];
+
+ public function log(...$args): void
+ {
+ $this->logs[] = $args;
+ }
+ };
+ $command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher(), $logger);
+
+ $application = new Application();
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
+ $tester = new CommandTester($application->get('messenger:consume'));
+ $tester->execute([
+ 'receivers' => ['dummy-receiver'],
+ '--memory-limit' => '1.5M',
+ ]);
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
+ $this->assertStringContainsString('The worker will automatically exit once it has exceeded 1.5M of memory', $tester->getDisplay());
+
+ $this->assertSame(1572864, $logger->logs[1][2]['limit']);
+ }
+
public function testRunWithAllOption()
{
$envelope1 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
@@ -232,7 +300,11 @@ public function testRunWithAllOption()
);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->get('messenger:consume'));
$tester->execute([
'--all' => true,
diff --git a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php
index f74661dc5ad1b..55e430c04497f 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php
@@ -176,7 +176,11 @@ public function testComplete(array $input, array $expectedSuggestions)
{
$command = new DebugCommand(['command_bus' => [], 'query_bus' => []]);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('debug:messenger'));
$this->assertSame($expectedSuggestions, $tester->complete($input));
}
diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json
index 94de9f2439c95..523513be77651 100644
--- a/src/Symfony/Component/Messenger/composer.json
+++ b/src/Symfony/Component/Messenger/composer.json
@@ -18,24 +18,24 @@
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
- "symfony/clock": "^6.4|^7.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
- "symfony/console": "^7.2",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/lock": "^6.4|^7.0",
- "symfony/rate-limiter": "^6.4|^7.0",
- "symfony/routing": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
+ "symfony/console": "^7.2|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/validator": "^6.4|^7.0"
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/console": "<7.2",
diff --git a/src/Symfony/Component/Mime/composer.json b/src/Symfony/Component/Mime/composer.json
index 5304bdf36d90b..e5cbc3cb651a4 100644
--- a/src/Symfony/Component/Mime/composer.json
+++ b/src/Symfony/Component/Mime/composer.json
@@ -24,11 +24,11 @@
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/property-info": "^6.4|^7.0",
- "symfony/serializer": "^6.4.3|^7.0.3"
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4.3|^7.0.3|^8.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
diff --git a/src/Symfony/Component/Notifier/Bridge/AllMySms/composer.json b/src/Symfony/Component/Notifier/Bridge/AllMySms/composer.json
index cdfed6cfc1f8c..d3e2a3756b51f 100644
--- a/src/Symfony/Component/Notifier/Bridge/AllMySms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/AllMySms/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\AllMySms\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/composer.json b/src/Symfony/Component/Notifier/Bridge/AmazonSns/composer.json
index 7c75c725424f1..8bbd2e750db1e 100644
--- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0",
"async-aws/sns": "^1.0"
},
"autoload": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Bandwidth/composer.json b/src/Symfony/Component/Notifier/Bridge/Bandwidth/composer.json
index 3dae426e42b46..4255e3d08a571 100644
--- a/src/Symfony/Component/Notifier/Bridge/Bandwidth/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Bandwidth/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Bandwidth\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json
index b6f2a542b6258..82aab39f5f248 100644
--- a/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json
@@ -22,13 +22,13 @@
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
- "symfony/clock": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3",
- "symfony/string": "^6.4|^7.0"
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0",
+ "symfony/string": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/mime": "^6.4|^7.0"
+ "symfony/mime": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Bluesky\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Brevo/composer.json b/src/Symfony/Component/Notifier/Bridge/Brevo/composer.json
index 96da9a51281de..fa530a5ebadab 100644
--- a/src/Symfony/Component/Notifier/Bridge/Brevo/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Brevo/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^5.4|^6.0|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Brevo\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json b/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json
index c1cbe7a01adaa..edc0c6395dc06 100644
--- a/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Chatwork/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Chatwork\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
index 67af3ac9237a7..65f48bcd7ac19 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
@@ -75,13 +75,13 @@ protected function doSend(MessageInterface $message): SentMessage
$options['from'] = $message->getFrom() ?: $this->from;
$options['source'] ??= $this->source;
$options['list_id'] ??= $this->listId;
- $options['from_email'] ?? $this->fromEmail;
+ $options['from_email'] ??= $this->fromEmail;
if (isset($options['from']) && !preg_match('/^[a-zA-Z0-9\s]{3,11}$/', $options['from']) && !preg_match('/^\+[1-9]\d{1,14}$/', $options['from'])) {
throw new InvalidArgumentException(\sprintf('The "From" number "%s" is not a valid phone number, shortcode, or alphanumeric sender ID.', $options['from']));
}
- if ($options['list_id'] ?? false) {
+ if (!$options['list_id']) {
$options['to'] = $message->getPhone();
}
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
index e1f9fa37dcae0..532c5aceba3aa 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
@@ -24,7 +24,7 @@
final class ClickSendTransportTest extends TransportTestCase
{
- public static function createTransport(?HttpClientInterface $client = null, string $from = 'test_from', string $source = 'test_source', int $listId = 99, string $fromEmail = 'foo@bar.com'): ClickSendTransport
+ public static function createTransport(?HttpClientInterface $client = null, ?string $from = 'test_from', ?string $source = 'test_source', ?int $listId = 99, ?string $fromEmail = 'foo@bar.com'): ClickSendTransport
{
return new ClickSendTransport('test_username', 'test_key', $from, $source, $listId, $fromEmail, $client ?? new MockHttpClient());
}
@@ -70,6 +70,10 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$body = json_decode($options['body'], true);
self::assertIsArray($body);
self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayHasKey('from_email', $message);
+ self::assertArrayHasKey('list_id', $message);
+ self::assertArrayNotHasKey('to', $message);
return $response;
});
@@ -77,6 +81,29 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$transport->send($message);
}
+ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValidWithoutOptionalParameters()
+ {
+ $message = new SmsMessage('+33612345678', 'Hello!');
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects(self::exactly(2))->method('getStatusCode')->willReturn(200);
+ $response->expects(self::once())->method('getContent')->willReturn('');
+ $client = new MockHttpClient(function (string $method, string $url, array $options) use ($response): ResponseInterface {
+ self::assertSame('POST', $method);
+ self::assertSame('https://rest.clicksend.com/v3/sms/send', $url);
+
+ $body = json_decode($options['body'], true);
+ self::assertIsArray($body);
+ self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayNotHasKey('list_id', $message);
+ self::assertArrayHasKey('to', $message);
+
+ return $response;
+ });
+ $transport = $this->createTransport($client, null, null, null, null);
+ $transport->send($message);
+ }
+
public static function toStringProvider(): iterable
{
yield ['clicksend://rest.clicksend.com?from=test_from&source=test_source&list_id=99&from_email=foo%40bar.com', self::createTransport()];
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/composer.json b/src/Symfony/Component/Notifier/Bridge/ClickSend/composer.json
index 1676fea9e458f..5f264d6403adf 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\ClickSend\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json
index 020ce41f9ca12..1f7c9d08b1605 100644
--- a/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Clickatell\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/ContactEveryone/composer.json b/src/Symfony/Component/Notifier/Bridge/ContactEveryone/composer.json
index 6e18ed4424747..3ccdab9f9ebc4 100644
--- a/src/Symfony/Component/Notifier/Bridge/ContactEveryone/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/ContactEveryone/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\ContactEveryone\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Discord/composer.json b/src/Symfony/Component/Notifier/Bridge/Discord/composer.json
index 4567a41f14f65..76ac74d512119 100644
--- a/src/Symfony/Component/Notifier/Bridge/Discord/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Discord/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0",
"symfony/polyfill-mbstring": "^1.0"
},
"autoload": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json b/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json
index 917b8304e9636..dd9be4b9bba3f 100644
--- a/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Engagespot\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json b/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json
index a7beb52075fa3..584c309000367 100644
--- a/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Esendex\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Expo/composer.json b/src/Symfony/Component/Notifier/Bridge/Expo/composer.json
index 002a08c0152a2..015e98d1f6c03 100644
--- a/src/Symfony/Component/Notifier/Bridge/Expo/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Expo/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Expo\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json b/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json
index 24e05807ec32d..447436ba0fd71 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json
@@ -22,12 +22,12 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/mailer": "^6.4|^7.0"
+ "symfony/mailer": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\FakeChat\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
index e652e879ed64f..e182e66fb848a 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
@@ -20,6 +19,7 @@
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
index ca90a24a449d9..7a37386875816 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
@@ -12,12 +12,12 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json b/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json
index 366ec1b8c48fb..4b5a022065e0f 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json
@@ -22,12 +22,12 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/mailer": "^6.4|^7.0"
+ "symfony/mailer": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\FakeSms\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json
index fa18127a3f874..af23aabbec7b8 100644
--- a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Firebase\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json b/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json
index 05a1311febf52..83991430bca7c 100644
--- a/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\FortySixElks\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/FreeMobile/composer.json b/src/Symfony/Component/Notifier/Bridge/FreeMobile/composer.json
index 8067f44f261f9..1853af7e319c7 100644
--- a/src/Symfony/Component/Notifier/Bridge/FreeMobile/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/FreeMobile/composer.json
@@ -18,8 +18,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\FreeMobile\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/GatewayApi/composer.json b/src/Symfony/Component/Notifier/Bridge/GatewayApi/composer.json
index 7abffe9ba4581..1a2f8290944f4 100644
--- a/src/Symfony/Component/Notifier/Bridge/GatewayApi/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/GatewayApi/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\GatewayApi\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/GoIp/composer.json b/src/Symfony/Component/Notifier/Bridge/GoIp/composer.json
index 166675db8ca9b..a643c08361450 100644
--- a/src/Symfony/Component/Notifier/Bridge/GoIp/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/GoIp/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json b/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json
index 645b5320b552a..37ab7ee264bbe 100644
--- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\GoogleChat\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Infobip/composer.json b/src/Symfony/Component/Notifier/Bridge/Infobip/composer.json
index a76a85aefd36b..15b41d40a2cd1 100644
--- a/src/Symfony/Component/Notifier/Bridge/Infobip/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Infobip/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Infobip\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Iqsms/composer.json b/src/Symfony/Component/Notifier/Bridge/Iqsms/composer.json
index d36db5d2bebf3..f18db7b4f44f8 100644
--- a/src/Symfony/Component/Notifier/Bridge/Iqsms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Iqsms/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Iqsms\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Isendpro/composer.json b/src/Symfony/Component/Notifier/Bridge/Isendpro/composer.json
index b7ee9fdbf95f1..efa8ecc0dde24 100644
--- a/src/Symfony/Component/Notifier/Bridge/Isendpro/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Isendpro/composer.json
@@ -21,11 +21,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Isendpro\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/JoliNotif/composer.json b/src/Symfony/Component/Notifier/Bridge/JoliNotif/composer.json
index e6512df786dc0..66e34613f96b2 100644
--- a/src/Symfony/Component/Notifier/Bridge/JoliNotif/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/JoliNotif/composer.json
@@ -23,8 +23,8 @@
"require": {
"php": ">=8.2",
"jolicode/jolinotif": "^2.7.2|^3.0",
- "symfony/http-client": "^7.2",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^7.2|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json b/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json
index aa2a2d126bf4c..38ea6acc5535b 100644
--- a/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json
@@ -19,8 +19,8 @@
"require": {
"php": ">=8.2",
"ext-simplexml": "*",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\KazInfoTeh\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json b/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json
index 18a3d52027894..9cb0e2e092ff6 100644
--- a/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LightSms\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/LineBot/composer.json b/src/Symfony/Component/Notifier/Bridge/LineBot/composer.json
index 7e200237c7cfa..f5bb1102e4f13 100644
--- a/src/Symfony/Component/Notifier/Bridge/LineBot/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/LineBot/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LineBot\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/LineNotify/composer.json b/src/Symfony/Component/Notifier/Bridge/LineNotify/composer.json
index c7af719ead66d..93aceb6388d60 100644
--- a/src/Symfony/Component/Notifier/Bridge/LineNotify/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/LineNotify/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LineNotify\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json b/src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json
index 2886f0eba9b68..dea4cd68b967e 100644
--- a/src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LinkedIn\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Lox24/composer.json b/src/Symfony/Component/Notifier/Bridge/Lox24/composer.json
index 98f09a409937d..3664936585b35 100644
--- a/src/Symfony/Component/Notifier/Bridge/Lox24/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Lox24/composer.json
@@ -10,12 +10,12 @@
"homepage": "https://symfony.com",
"license": "MIT",
"require": {
- "php": ">=8.1",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "php": ">=8.2",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Mailjet/composer.json b/src/Symfony/Component/Notifier/Bridge/Mailjet/composer.json
index 9aa215e815fb2..fdf8269bf2360 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mailjet/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Mailjet/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mailjet\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Mastodon/composer.json b/src/Symfony/Component/Notifier/Bridge/Mastodon/composer.json
index d09d403fc7b36..190e279e84f4d 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mastodon/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Mastodon/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/mime": "^6.4|^7.0"
+ "symfony/mime": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mastodon\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json b/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json
index 22ce807af3364..f51c3804bfae7 100644
--- a/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Matrix/composer.json
@@ -17,9 +17,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/notifier": "^7.3",
- "symfony/uid": "^7.2",
- "symfony/http-client": "^6.4|^7.0"
+ "symfony/notifier": "^7.3|^8.0",
+ "symfony/uid": "^7.2|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Mattermost/composer.json b/src/Symfony/Component/Notifier/Bridge/Mattermost/composer.json
index 958d64f42f865..0a0a72c535669 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mattermost/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Mattermost/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mattermost\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json b/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json
index ac965af31ca78..9920fe60f2abd 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json
@@ -18,7 +18,7 @@
"require": {
"php": ">=8.2",
"symfony/mercure": "^0.5.2|^0.6",
- "symfony/notifier": "^7.3",
+ "symfony/notifier": "^7.3|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"autoload": {
diff --git a/src/Symfony/Component/Notifier/Bridge/MessageBird/composer.json b/src/Symfony/Component/Notifier/Bridge/MessageBird/composer.json
index c1729e047f3fd..bffc9b345c13a 100644
--- a/src/Symfony/Component/Notifier/Bridge/MessageBird/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/MessageBird/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MessageBird\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json
index 187f9ed1fde88..e46a9e69de6fa 100644
--- a/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MessageMedia\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/composer.json b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/composer.json
index 37722b03ec16a..28b83814f49ee 100644
--- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MicrosoftTeams\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Mobyt/composer.json b/src/Symfony/Component/Notifier/Bridge/Mobyt/composer.json
index 1317236985478..1f14286f128a6 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mobyt/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Mobyt/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Mobyt\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Novu/composer.json b/src/Symfony/Component/Notifier/Bridge/Novu/composer.json
index 999320b21523b..7a303afdb4aca 100644
--- a/src/Symfony/Component/Notifier/Bridge/Novu/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Novu/composer.json
@@ -16,9 +16,9 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/http-client": "^5.4|^6.0|^7.0",
- "symfony/notifier": "^7.2"
+ "php": ">=8.2",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Novu\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json b/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json
index e15e8d511b973..6e7c25249dd27 100644
--- a/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json
@@ -17,9 +17,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/clock": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Ntfy\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Octopush/composer.json b/src/Symfony/Component/Notifier/Bridge/Octopush/composer.json
index d081b539bc179..91f9e9fc6d7a0 100644
--- a/src/Symfony/Component/Notifier/Bridge/Octopush/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Octopush/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Octopush\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json
index 2d3d243cf3884..35be562c547d6 100644
--- a/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\OneSignal\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/OrangeSms/composer.json b/src/Symfony/Component/Notifier/Bridge/OrangeSms/composer.json
index 24923f1bc0bb9..a26866587b53b 100644
--- a/src/Symfony/Component/Notifier/Bridge/OrangeSms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/OrangeSms/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\OrangeSms\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/OvhCloud/composer.json b/src/Symfony/Component/Notifier/Bridge/OvhCloud/composer.json
index c105fcccdf9e0..ae82ed77dcc81 100644
--- a/src/Symfony/Component/Notifier/Bridge/OvhCloud/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/OvhCloud/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\OvhCloud\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/PagerDuty/composer.json b/src/Symfony/Component/Notifier/Bridge/PagerDuty/composer.json
index b75ee3960c62a..f1f14ae047d52 100644
--- a/src/Symfony/Component/Notifier/Bridge/PagerDuty/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/PagerDuty/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\PagerDuty\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Plivo/composer.json b/src/Symfony/Component/Notifier/Bridge/Plivo/composer.json
index 4a4c3cb13fd21..ead7c057ae552 100644
--- a/src/Symfony/Component/Notifier/Bridge/Plivo/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Plivo/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Plivo\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Primotexto/composer.json b/src/Symfony/Component/Notifier/Bridge/Primotexto/composer.json
index e89e378e144e0..094a05b1e321d 100644
--- a/src/Symfony/Component/Notifier/Bridge/Primotexto/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Primotexto/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Primotexto\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Pushover/composer.json b/src/Symfony/Component/Notifier/Bridge/Pushover/composer.json
index 926267eee9dc8..70c14694afe0a 100644
--- a/src/Symfony/Component/Notifier/Bridge/Pushover/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Pushover/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Pushover\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Pushy/composer.json b/src/Symfony/Component/Notifier/Bridge/Pushy/composer.json
index e207e4b3a2811..e774ee4c52b71 100644
--- a/src/Symfony/Component/Notifier/Bridge/Pushy/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Pushy/composer.json
@@ -21,11 +21,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Pushy\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Redlink/composer.json b/src/Symfony/Component/Notifier/Bridge/Redlink/composer.json
index e56215f7b36ba..6398c1ea913ef 100644
--- a/src/Symfony/Component/Notifier/Bridge/Redlink/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Redlink/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Redlink\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/RingCentral/composer.json b/src/Symfony/Component/Notifier/Bridge/RingCentral/composer.json
index df9f13c56189c..c05948b79acdb 100644
--- a/src/Symfony/Component/Notifier/Bridge/RingCentral/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/RingCentral/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\RingCentral\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/RocketChat/composer.json b/src/Symfony/Component/Notifier/Bridge/RocketChat/composer.json
index bc7bd923340a8..31e312222c67d 100644
--- a/src/Symfony/Component/Notifier/Bridge/RocketChat/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/RocketChat/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\RocketChat\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json b/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json
index 56a9e2163023e..2dcbd77c51b2b 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sendberry\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json b/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json
index c2b6d0b5264c7..2c489b47fb50b 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sevenio\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/SimpleTextin/composer.json b/src/Symfony/Component/Notifier/Bridge/SimpleTextin/composer.json
index f27e41c7b090a..8e1e6799135bb 100644
--- a/src/Symfony/Component/Notifier/Bridge/SimpleTextin/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/SimpleTextin/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SimpleTextin\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sinch/composer.json b/src/Symfony/Component/Notifier/Bridge/Sinch/composer.json
index 296393553b02d..8128c5bfa780d 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sinch/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sinch/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sinch\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json b/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json
index bba4c1bb1b652..12ffb1f792d82 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackActionsBlock.php b/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackActionsBlock.php
index 25da853677415..848b9bfd98f83 100644
--- a/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackActionsBlock.php
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackActionsBlock.php
@@ -24,16 +24,26 @@ public function __construct()
/**
* @return $this
*/
- public function button(string $text, string $url, ?string $style = null): static
+ public function button(string $text, ?string $url = null, ?string $style = null, ?string $value = null): static
{
if (25 === \count($this->options['elements'] ?? [])) {
throw new \LogicException('Maximum number of buttons should not exceed 25.');
}
- $element = new SlackButtonBlockElement($text, $url, $style);
+ $element = new SlackButtonBlockElement($text, $url, $style, $value);
$this->options['elements'][] = $element->toArray();
return $this;
}
+
+ /**
+ * @return $this
+ */
+ public function id(string $id): static
+ {
+ $this->options['block_id'] = $id;
+
+ return $this;
+ }
}
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackButtonBlockElement.php b/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackButtonBlockElement.php
index ff83bb9d870a5..8b1eed19472cb 100644
--- a/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackButtonBlockElement.php
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/Block/SlackButtonBlockElement.php
@@ -16,7 +16,7 @@
*/
final class SlackButtonBlockElement extends AbstractSlackBlockElement
{
- public function __construct(string $text, string $url, ?string $style = null)
+ public function __construct(string $text, ?string $url = null, ?string $style = null, ?string $value = null)
{
$this->options = [
'type' => 'button',
@@ -24,12 +24,19 @@ public function __construct(string $text, string $url, ?string $style = null)
'type' => 'plain_text',
'text' => $text,
],
- 'url' => $url,
];
+ if ($url) {
+ $this->options['url'] = $url;
+ }
+
if ($style) {
// primary or danger
$this->options['style'] = $style;
}
+
+ if ($value) {
+ $this->options['value'] = $value;
+ }
}
}
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/Block/SlackActionsBlockTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/Block/SlackActionsBlockTest.php
index 2a21a39133c1f..682a895bdffc0 100644
--- a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/Block/SlackActionsBlockTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/Block/SlackActionsBlockTest.php
@@ -19,8 +19,9 @@ final class SlackActionsBlockTest extends TestCase
public function testCanBeInstantiated()
{
$actions = new SlackActionsBlock();
- $actions->button('first button text', 'https://example.org')
+ $actions->button('first button text', 'https://example.org', null, 'test-value')
->button('second button text', 'https://example.org/slack', 'danger')
+ ->button('third button text', null, null, 'test-value-3')
;
$this->assertSame([
@@ -33,6 +34,7 @@ public function testCanBeInstantiated()
'text' => 'first button text',
],
'url' => 'https://example.org',
+ 'value' => 'test-value'
],
[
'type' => 'button',
@@ -43,6 +45,14 @@ public function testCanBeInstantiated()
'url' => 'https://example.org/slack',
'style' => 'danger',
],
+ [
+ 'type' => 'button',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => 'third button text',
+ ],
+ 'value' => 'test-value-3',
+ ]
],
], $actions->toArray());
}
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
index 8507a4d041254..bb6752dd37080 100644
--- a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Slack\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sms77/composer.json b/src/Symfony/Component/Notifier/Bridge/Sms77/composer.json
index 8dd642e151321..25def742454a1 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sms77/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sms77/composer.json
@@ -18,8 +18,8 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sms77\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/composer.json b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/composer.json
index cbba623e99aa4..feff4ced7eede 100644
--- a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SmsBiuras\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/SmsFactor/composer.json b/src/Symfony/Component/Notifier/Bridge/SmsFactor/composer.json
index b530771b49dce..5496b1185c5eb 100644
--- a/src/Symfony/Component/Notifier/Bridge/SmsFactor/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/SmsFactor/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SmsFactor\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json
index c4256019769a0..fd419bf91fcf7 100644
--- a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json
@@ -16,10 +16,10 @@
}
],
"require": {
- "php": ">=8.1",
- "symfony/http-client": "^6.4|^7.1",
- "symfony/notifier": "^7.2",
- "symfony/serializer": "^6.4|^7.1"
+ "php": ">=8.2",
+ "symfony/http-client": "^6.4|^7.1|^8.0",
+ "symfony/notifier": "^7.2|^8.0",
+ "symfony/serializer": "^6.4|^7.1|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SmsSluzba\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsapi/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsapi/composer.json
index 40e2710c4dff7..481ac4538c99f 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsapi/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Smsapi/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.3"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.3|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Smsapi\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json
index 0b1907fb71f15..2c6d8e9cc0242 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json
@@ -25,13 +25,13 @@
],
"require": {
"php": ">=8.2",
- "symfony/clock": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0",
"symfony/polyfill-php83": "^1.28"
},
"require-dev": {
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsc/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsc/composer.json
index 2a5bded5aea9b..f057349e0e65a 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsc/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Smsc/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Smsc\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsense/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsense/composer.json
index 4002648bd504b..f80bd05867d3c 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsense/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Smsense/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Smsense\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsmode/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsmode/composer.json
index a9131f75ed3ad..f975c19833ccd 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsmode/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Smsmode/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Smsmode\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json b/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json
index a9b66ba3636b4..491df32dedded 100644
--- a/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SpotHit\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json
index 006d739b86151..f9c3dc1d26f18 100644
--- a/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<7.1"
diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
index 435641839410a..e046bcfd320e5 100644
--- a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
@@ -17,9 +17,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Telegram\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Telnyx/composer.json b/src/Symfony/Component/Notifier/Bridge/Telnyx/composer.json
index 3b53e750bd395..860c0f7f9efd2 100644
--- a/src/Symfony/Component/Notifier/Bridge/Telnyx/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Telnyx/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Telnyx\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Termii/composer.json b/src/Symfony/Component/Notifier/Bridge/Termii/composer.json
index 31ed79a368071..57d397d206c9b 100644
--- a/src/Symfony/Component/Notifier/Bridge/Termii/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Termii/composer.json
@@ -20,11 +20,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0"
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Termii\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/TurboSms/composer.json b/src/Symfony/Component/Notifier/Bridge/TurboSms/composer.json
index 36c6def23ae2d..d90400dad8aca 100644
--- a/src/Symfony/Component/Notifier/Bridge/TurboSms/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/TurboSms/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0",
"symfony/polyfill-mbstring": "^1.0"
},
"autoload": {
diff --git a/src/Symfony/Component/Notifier/Bridge/Twilio/composer.json b/src/Symfony/Component/Notifier/Bridge/Twilio/composer.json
index ee1872491bdfb..f2ae0c75429ca 100644
--- a/src/Symfony/Component/Notifier/Bridge/Twilio/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Twilio/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
diff --git a/src/Symfony/Component/Notifier/Bridge/Twitter/composer.json b/src/Symfony/Component/Notifier/Bridge/Twitter/composer.json
index f50531a1448ed..2f96a28349f40 100644
--- a/src/Symfony/Component/Notifier/Bridge/Twitter/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Twitter/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/mime": "^6.4|^7.0"
+ "symfony/mime": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/mime": "<6.4"
diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json b/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json
index 48fbbdf2db84b..30cad613f85af 100644
--- a/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json
@@ -21,8 +21,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Unifonic\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json b/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json
index 243f0903155fe..a8939fb564e88 100644
--- a/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"require-dev": {
- "symfony/webhook": "^6.4|^7.0"
+ "symfony/webhook": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Vonage\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Yunpian/composer.json b/src/Symfony/Component/Notifier/Bridge/Yunpian/composer.json
index 58366edc8eb74..4bf05c1afc0cd 100644
--- a/src/Symfony/Component/Notifier/Bridge/Yunpian/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Yunpian/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Yunpian\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Zendesk/composer.json b/src/Symfony/Component/Notifier/Bridge/Zendesk/composer.json
index 87044d4d4829e..66eea8847150d 100644
--- a/src/Symfony/Component/Notifier/Bridge/Zendesk/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Zendesk/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Zendesk\\": "" },
diff --git a/src/Symfony/Component/Notifier/Bridge/Zulip/composer.json b/src/Symfony/Component/Notifier/Bridge/Zulip/composer.json
index f124c18e7e58b..8fb05ff6b2817 100644
--- a/src/Symfony/Component/Notifier/Bridge/Zulip/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Zulip/composer.json
@@ -17,8 +17,8 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/notifier": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/notifier": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Zulip\\": "" },
diff --git a/src/Symfony/Component/Notifier/composer.json b/src/Symfony/Component/Notifier/composer.json
index 3cb8fe7d28073..9cc0495f4d396 100644
--- a/src/Symfony/Component/Notifier/composer.json
+++ b/src/Symfony/Component/Notifier/composer.json
@@ -22,8 +22,8 @@
"require-dev": {
"symfony/event-dispatcher-contracts": "^2.5|^3",
"symfony/http-client-contracts": "^2.5|^3",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0"
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/event-dispatcher": "<6.4",
diff --git a/src/Symfony/Component/ObjectMapper/ObjectMapper.php b/src/Symfony/Component/ObjectMapper/ObjectMapper.php
index d78bc3ce8d216..69f02fb7f1160 100644
--- a/src/Symfony/Component/ObjectMapper/ObjectMapper.php
+++ b/src/Symfony/Component/ObjectMapper/ObjectMapper.php
@@ -70,7 +70,7 @@ public function map(object $source, object|string|null $target = null): object
$mappedTarget = $mappingToObject ? $target : $targetRefl->newInstanceWithoutConstructor();
if ($map && $map->transform) {
- $mappedTarget = $this->applyTransforms($map, $mappedTarget, $mappedTarget, null);
+ $mappedTarget = $this->applyTransforms($map, $mappedTarget, $source, null);
if (!\is_object($mappedTarget)) {
throw new MappingTransformException(\sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget)));
diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php
new file mode 100644
index 0000000000000..77ab0c3a3a76e
--- /dev/null
+++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments;
+
+use Symfony\Component\ObjectMapper\Attribute\Map;
+
+#[Map(target: B::class, transform: [B::class, 'newInstance'])]
+class A
+{
+}
diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php
new file mode 100644
index 0000000000000..b5ea60066b59f
--- /dev/null
+++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments;
+
+class B
+{
+ public mixed $transformValue;
+ public object $transformSource;
+
+ public static function newInstance(mixed $value, object $source): self
+ {
+ $b = new self();
+ $b->transformValue = $value;
+ $b->transformSource = $source;
+
+ return $b;
+ }
+}
diff --git a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
index a416abd47933b..99153c3fbdfc7 100644
--- a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
+++ b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
@@ -34,6 +34,8 @@
use Symfony\Component\ObjectMapper\Tests\Fixtures\HydrateObject\SourceOnly;
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\A as InstanceCallbackA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\B as InstanceCallbackB;
+use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments\A as InstanceCallbackWithArgumentsA;
+use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments\B as InstanceCallbackWithArgumentsB;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\AToBMapper;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\MapStructMapperMetadataFactory;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Source;
@@ -155,6 +157,16 @@ public function testMapToWithInstanceHook()
$this->assertSame($b->name, 'test');
}
+ public function testMapToWithInstanceHookWithArguments()
+ {
+ $a = new InstanceCallbackWithArgumentsA();
+ $mapper = new ObjectMapper();
+ $b = $mapper->map($a);
+ $this->assertInstanceOf(InstanceCallbackWithArgumentsB::class, $b);
+ $this->assertSame($a, $b->transformSource);
+ $this->assertInstanceOf(InstanceCallbackWithArgumentsB::class, $b->transformValue);
+ }
+
public function testMapStruct()
{
$a = new Source('a', 'b', 'c');
@@ -284,11 +296,11 @@ public function testMultipleTargetMapProperty()
$mapper = new ObjectMapper();
$b = $mapper->map($u, MultipleTargetPropertyB::class);
$this->assertInstanceOf(MultipleTargetPropertyB::class, $b);
- $this->assertEquals($b->foo, 'TEST');
+ $this->assertEquals('TEST', $b->foo);
$c = $mapper->map($u, MultipleTargetPropertyC::class);
$this->assertInstanceOf(MultipleTargetPropertyC::class, $c);
- $this->assertEquals($c->bar, 'test');
- $this->assertEquals($c->foo, 'donotmap');
- $this->assertEquals($c->doesNotExistInTargetB, 'foo');
+ $this->assertEquals('test', $c->bar);
+ $this->assertEquals('donotmap', $c->foo);
+ $this->assertEquals('foo', $c->doesNotExistInTargetB);
}
}
diff --git a/src/Symfony/Component/ObjectMapper/composer.json b/src/Symfony/Component/ObjectMapper/composer.json
index eb89582d8aad6..1ae9f2740fea2 100644
--- a/src/Symfony/Component/ObjectMapper/composer.json
+++ b/src/Symfony/Component/ObjectMapper/composer.json
@@ -20,7 +20,7 @@
"psr/container": "^2.0"
},
"require-dev": {
- "symfony/property-access": "^7.2"
+ "symfony/property-access": "^7.2|^8.0"
},
"autoload": {
"psr-4": {
@@ -32,5 +32,6 @@
},
"conflict": {
"symfony/property-access": "<7.2"
- }
+ },
+ "minimum-stability": "dev"
}
diff --git a/src/Symfony/Component/PasswordHasher/composer.json b/src/Symfony/Component/PasswordHasher/composer.json
index ebcb51b4b9599..1eb6681722c02 100644
--- a/src/Symfony/Component/PasswordHasher/composer.json
+++ b/src/Symfony/Component/PasswordHasher/composer.json
@@ -19,8 +19,8 @@
"php": ">=8.2"
},
"require-dev": {
- "symfony/security-core": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0"
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/security-core": "<6.4"
diff --git a/src/Symfony/Component/Process/PhpProcess.php b/src/Symfony/Component/Process/PhpProcess.php
index 0e7ff84647fb8..930f591f0d399 100644
--- a/src/Symfony/Component/Process/PhpProcess.php
+++ b/src/Symfony/Component/Process/PhpProcess.php
@@ -55,6 +55,9 @@ public static function fromShellCommandline(string $command, ?string $cwd = null
throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
}
+ /**
+ * @param (callable('out'|'err', string):void)|null $callback
+ */
public function start(?callable $callback = null, array $env = []): void
{
if (null === $this->getCommandLine()) {
diff --git a/src/Symfony/Component/Process/PhpSubprocess.php b/src/Symfony/Component/Process/PhpSubprocess.php
index bdd4173c2a053..8282f93cd47ea 100644
--- a/src/Symfony/Component/Process/PhpSubprocess.php
+++ b/src/Symfony/Component/Process/PhpSubprocess.php
@@ -78,6 +78,9 @@ public static function fromShellCommandline(string $command, ?string $cwd = null
throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
}
+ /**
+ * @param (callable('out'|'err', string):void)|null $callback
+ */
public function start(?callable $callback = null, array $env = []): void
{
if (null === $this->getCommandLine()) {
diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php
index a8beb93d44988..d52db23ac6afb 100644
--- a/src/Symfony/Component/Process/Process.php
+++ b/src/Symfony/Component/Process/Process.php
@@ -51,6 +51,9 @@ class Process implements \IteratorAggregate
public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating
public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating
+ /**
+ * @var \Closure('out'|'err', string)|null
+ */
private ?\Closure $callback = null;
private array|string $commandline;
private ?string $cwd;
@@ -231,8 +234,8 @@ public function __clone()
* The STDOUT and STDERR are also available after the process is finished
* via the getOutput() and getErrorOutput() methods.
*
- * @param callable|null $callback A PHP callback to run whenever there is some
- * output available on STDOUT or STDERR
+ * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
*
* @return int The exit status code
*
@@ -257,6 +260,9 @@ public function run(?callable $callback = null, array $env = []): int
* This is identical to run() except that an exception is thrown if the process
* exits with a non-zero exit code.
*
+ * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
+ *
* @return $this
*
* @throws ProcessFailedException if the process didn't terminate successfully
@@ -284,8 +290,8 @@ public function mustRun(?callable $callback = null, array $env = []): static
* the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
- * @param callable|null $callback A PHP callback to run whenever there is some
- * output available on STDOUT or STDERR
+ * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
*
* @throws ProcessStartFailedException When process can't be launched
* @throws RuntimeException When process is already running
@@ -395,8 +401,8 @@ public function start(?callable $callback = null, array $env = []): void
*
* Be warned that the process is cloned before being started.
*
- * @param callable|null $callback A PHP callback to run whenever there is some
- * output available on STDOUT or STDERR
+ * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
*
* @throws ProcessStartFailedException When process can't be launched
* @throws RuntimeException When process is already running
@@ -424,7 +430,8 @@ public function restart(?callable $callback = null, array $env = []): static
* from the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
- * @param callable|null $callback A valid PHP callback
+ * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
*
* @return int The exitcode of the process
*
@@ -471,6 +478,9 @@ public function wait(?callable $callback = null): int
* from the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
+ * @param (callable('out'|'err', string):bool)|null $callback A PHP callback to run whenever there is some
+ * output available on STDOUT or STDERR
+ *
* @throws RuntimeException When process timed out
* @throws LogicException When process is not yet started
* @throws ProcessTimedOutException In case the timeout was reached
@@ -1291,7 +1301,9 @@ private function getDescriptors(bool $hasCallback): array
* The callbacks adds all occurred output to the specific buffer and calls
* the user callback (if present) with the received output.
*
- * @param callable|null $callback The user defined PHP callback
+ * @param callable('out'|'err', string)|null $callback
+ *
+ * @return \Closure('out'|'err', string):bool
*/
protected function buildCallback(?callable $callback = null): \Closure
{
@@ -1299,14 +1311,11 @@ protected function buildCallback(?callable $callback = null): \Closure
return fn ($type, $data): bool => null !== $callback && $callback($type, $data);
}
- $out = self::OUT;
-
- return function ($type, $data) use ($callback, $out): bool {
- if ($out == $type) {
- $this->addOutput($data);
- } else {
- $this->addErrorOutput($data);
- }
+ return function ($type, $data) use ($callback): bool {
+ match ($type) {
+ self::OUT => $this->addOutput($data),
+ self::ERR => $this->addErrorOutput($data),
+ };
return null !== $callback && $callback($type, $data);
};
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
index d34e19f0c9b19..7066e1545e7d6 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
@@ -109,11 +109,6 @@ public function getValue(object|array $objectOrArray, string|PropertyPathInterfa
return $propertyValues[\count($propertyValues) - 1][self::VALUE];
}
- /**
- * @template T of object|array
- * @param T $objectOrArray
- * @param-out ($objectOrArray is array ? array : T) $objectOrArray
- */
public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void
{
if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) {
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
index 2e25e9e517db2..ccbaf8b3c4b49 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
@@ -39,6 +39,12 @@ interface PropertyAccessorInterface
*
* If neither is found, an exception is thrown.
*
+ * @template T of object|array
+ *
+ * @param T $objectOrArray
+ *
+ * @param-out ($objectOrArray is array ? array : T) $objectOrArray
+ *
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json
index 376ee7e1afd0d..906e432b03f99 100644
--- a/src/Symfony/Component/PropertyAccess/composer.json
+++ b/src/Symfony/Component/PropertyAccess/composer.json
@@ -17,10 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/property-info": "^6.4|^7.0"
+ "symfony/property-info": "^6.4|^7.0|^8.0"
},
"require-dev": {
- "symfony/cache": "^6.4|^7.0"
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\PropertyAccess\\": "" },
diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json
index f8ae018a40b7f..89c2c6ad01cce 100644
--- a/src/Symfony/Component/PropertyInfo/composer.json
+++ b/src/Symfony/Component/PropertyInfo/composer.json
@@ -25,13 +25,13 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/string": "^6.4|^7.0",
- "symfony/type-info": "~7.1.9|^7.2.2"
+ "symfony/string": "^6.4|^7.0|^8.0",
+ "symfony/type-info": "~7.1.9|^7.2.2|^8.0"
},
"require-dev": {
- "symfony/serializer": "^6.4|^7.0",
- "symfony/cache": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.0|^2.0"
},
diff --git a/src/Symfony/Component/RateLimiter/composer.json b/src/Symfony/Component/RateLimiter/composer.json
index fdf0e01c4979b..428ce3480e53f 100644
--- a/src/Symfony/Component/RateLimiter/composer.json
+++ b/src/Symfony/Component/RateLimiter/composer.json
@@ -17,11 +17,11 @@
],
"require": {
"php": ">=8.2",
- "symfony/options-resolver": "^7.3"
+ "symfony/options-resolver": "^7.3|^8.0"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
- "symfony/lock": "^6.4|^7.0"
+ "symfony/lock": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\RateLimiter\\": "" },
diff --git a/src/Symfony/Component/RemoteEvent/composer.json b/src/Symfony/Component/RemoteEvent/composer.json
index 292110b3424f5..83b82a71727e7 100644
--- a/src/Symfony/Component/RemoteEvent/composer.json
+++ b/src/Symfony/Component/RemoteEvent/composer.json
@@ -17,7 +17,7 @@
],
"require": {
"php": ">=8.2",
- "symfony/messenger": "^6.4|^7.0"
+ "symfony/messenger": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\RemoteEvent\\": "" },
diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md
index d21e550f9b57f..4ef96d53232fe 100644
--- a/src/Symfony/Component/Routing/CHANGELOG.md
+++ b/src/Symfony/Component/Routing/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.4
+---
+
+ * Allow query-specific parameters in `UrlGenerator` using `_query`
+
7.3
---
diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php
index 216b0d5479ac4..d82b91898194a 100644
--- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php
+++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php
@@ -142,6 +142,18 @@ public function generate(string $name, array $parameters = [], int $referenceTyp
*/
protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string
{
+ $queryParameters = [];
+
+ if (isset($parameters['_query'])) {
+ if (\is_array($parameters['_query'])) {
+ $queryParameters = $parameters['_query'];
+ unset($parameters['_query']);
+ } else {
+ trigger_deprecation('symfony/routing', '7.4', 'Parameter "_query" is reserved for passing an array of query parameters. Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.');
+ // throw new InvalidParameterException('Parameter "_query" must be an array of query parameters.');
+ }
+ }
+
$variables = array_flip($variables);
$mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
@@ -260,6 +272,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem
// add a query string if needed
$extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1);
+ $extra = array_merge($extra, $queryParameters);
array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) {
if (\is_object($v)) {
diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
index 25a4c67460c82..75196bd214aa2 100644
--- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
+++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
@@ -1054,6 +1054,80 @@ public function testUtf8VarName()
$this->assertSame('/app.php/foo/baz', $this->getGenerator($routes)->generate('test', ['bär' => 'baz']));
}
+ public function testQueryParameters()
+ {
+ $routes = $this->getRoutes('user', new Route('/user/{username}'));
+ $url = $this->getGenerator($routes)->generate('user', [
+ 'username' => 'john',
+ 'a' => 'foo',
+ 'b' => 'bar',
+ 'c' => 'baz',
+ '_query' => [
+ 'a' => '123',
+ 'd' => '789',
+ ],
+ ]);
+ $this->assertSame('/app.php/user/john?a=123&b=bar&c=baz&d=789', $url);
+ }
+
+ public function testRouteHostParameterAndQueryParameterWithSameName()
+ {
+ $routes = $this->getRoutes('admin_stats', new Route('/admin/stats', requirements: ['domain' => '.+'], host: '{siteCode}.{domain}'));
+ $url = $this->getGenerator($routes)->generate('admin_stats', [
+ 'siteCode' => 'fr',
+ 'domain' => 'example.com',
+ '_query' => [
+ 'siteCode' => 'us',
+ ],
+ ], UrlGeneratorInterface::NETWORK_PATH);
+ $this->assertSame('//fr.example.com/app.php/admin/stats?siteCode=us', $url);
+ }
+
+ public function testRoutePathParameterAndQueryParameterWithSameName()
+ {
+ $routes = $this->getRoutes('user', new Route('/user/{id}'));
+ $url = $this->getGenerator($routes)->generate('user', [
+ 'id' => '123',
+ '_query' => [
+ 'id' => '456',
+ ],
+ ]);
+ $this->assertSame('/app.php/user/123?id=456', $url);
+ }
+
+ public function testQueryParameterCannotSubstituteRouteParameter()
+ {
+ $routes = $this->getRoutes('user', new Route('/user/{id}'));
+
+ $this->expectException(MissingMandatoryParametersException::class);
+ $this->expectExceptionMessage('Some mandatory parameters are missing ("id") to generate a URL for route "user".');
+
+ $this->getGenerator($routes)->generate('user', [
+ '_query' => [
+ 'id' => '456',
+ ],
+ ]);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testQueryParametersWithScalarValue()
+ {
+ $routes = $this->getRoutes('user', new Route('/user/{id}'));
+
+ $this->expectUserDeprecationMessage(
+ 'Since symfony/routing 7.4: Parameter "_query" is reserved for passing an array of query parameters. ' .
+ 'Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.',
+ );
+
+ $url = $this->getGenerator($routes)->generate('user', [
+ 'id' => '123',
+ '_query' => 'foo',
+ ]);
+ $this->assertSame('/app.php/user/123?_query=foo', $url);
+ }
+
protected function getGenerator(RouteCollection $routes, array $parameters = [], $logger = null, ?string $defaultLocale = null)
{
$context = new RequestContext('/app.php');
diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json
index 59e30bef69611..1fcc24b61606c 100644
--- a/src/Symfony/Component/Routing/composer.json
+++ b/src/Symfony/Component/Routing/composer.json
@@ -20,11 +20,11 @@
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/config": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/yaml": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
diff --git a/src/Symfony/Component/Runtime/CHANGELOG.md b/src/Symfony/Component/Runtime/CHANGELOG.md
index 1a608b4cf734b..05cbfe9bc5287 100644
--- a/src/Symfony/Component/Runtime/CHANGELOG.md
+++ b/src/Symfony/Component/Runtime/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+7.4
+---
+
+ * Add `FrankenPhpWorkerRunner`
+ * Add automatic detection of FrankenPHP worker mode in `SymfonyRuntime`
+
6.4
---
diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
index a252814570f2e..c0c290e686800 100644
--- a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
@@ -20,7 +20,7 @@ class BasicErrorHandler
{
public static function register(bool $debug): void
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
index 0dfc7de0ca7a0..47c67605b0430 100644
--- a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
@@ -30,7 +30,7 @@ public static function register(bool $debug): void
return;
}
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/Runner/FrankenPhpWorkerRunner.php b/src/Symfony/Component/Runtime/Runner/FrankenPhpWorkerRunner.php
new file mode 100644
index 0000000000000..4d44791775cab
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Runner/FrankenPhpWorkerRunner.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Runner;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\TerminableInterface;
+use Symfony\Component\Runtime\RunnerInterface;
+
+/**
+ * A runner for FrankenPHP in worker mode.
+ *
+ * @author Kévin Dunglas
+ */
+class FrankenPhpWorkerRunner implements RunnerInterface
+{
+ public function __construct(
+ private HttpKernelInterface $kernel,
+ private int $loopMax,
+ ) {
+ }
+
+ public function run(): int
+ {
+ // Prevent worker script termination when a client connection is interrupted
+ ignore_user_abort(true);
+
+ $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY);
+ $server['APP_RUNTIME_MODE'] = 'web=1&worker=1';
+
+ $handler = function () use ($server, &$sfRequest, &$sfResponse): void {
+ // Connect to the Xdebug client if it's available
+ if (\extension_loaded('xdebug') && \function_exists('xdebug_connect_to_client')) {
+ xdebug_connect_to_client();
+ }
+
+ // Merge the environment variables coming from DotEnv with the ones tied to the current request
+ $_SERVER += $server;
+
+ $sfRequest = Request::createFromGlobals();
+ $sfResponse = $this->kernel->handle($sfRequest);
+
+ $sfResponse->send();
+ };
+
+ $loops = 0;
+ do {
+ $ret = \frankenphp_handle_request($handler);
+
+ if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) {
+ $this->kernel->terminate($sfRequest, $sfResponse);
+ }
+
+ gc_collect_cycles();
+ } while ($ret && (0 >= $this->loopMax || ++$loops < $this->loopMax));
+
+ return 0;
+ }
+}
diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php
index 4035f28c806cd..de130eaae8a13 100644
--- a/src/Symfony/Component/Runtime/SymfonyRuntime.php
+++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php
@@ -23,6 +23,7 @@
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Runtime\Internal\MissingDotenv;
use Symfony\Component\Runtime\Internal\SymfonyErrorHandler;
+use Symfony\Component\Runtime\Runner\FrankenPhpWorkerRunner;
use Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner;
use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner;
use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner;
@@ -42,6 +43,7 @@ class_exists(MissingDotenv::class, false) || class_exists(Dotenv::class) || clas
* - "use_putenv" to tell Dotenv to set env vars using putenv() (NOT RECOMMENDED.)
* - "dotenv_overload" to tell Dotenv to override existing vars
* - "dotenv_extra_paths" to define a list of additional dot-env files
+ * - "worker_loop_max" to define the number of requests after which the worker must restart to prevent memory leaks
*
* When the "debug" / "env" options are not defined, they will fallback to the
* "APP_DEBUG" / "APP_ENV" environment variables, and to the "--env|-e" / "--no-debug"
@@ -73,7 +75,7 @@ class SymfonyRuntime extends GenericRuntime
private readonly Command $command;
/**
- * @param array {
+ * @param array{
* debug?: ?bool,
* env?: ?string,
* disable_dotenv?: ?bool,
@@ -88,6 +90,7 @@ class SymfonyRuntime extends GenericRuntime
* debug_var_name?: string,
* dotenv_overload?: ?bool,
* dotenv_extra_paths?: ?string[],
+ * worker_loop_max?: int, // Use 0 or a negative integer to never restart the worker. Default: 500
* } $options
*/
public function __construct(array $options = [])
@@ -143,12 +146,23 @@ public function __construct(array $options = [])
$options['error_handler'] ??= SymfonyErrorHandler::class;
+ $workerLoopMax = $options['worker_loop_max'] ?? $_SERVER['FRANKENPHP_LOOP_MAX'] ?? $_ENV['FRANKENPHP_LOOP_MAX'] ?? null;
+ if (null !== $workerLoopMax && null === filter_var($workerLoopMax, \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE)) {
+ throw new \LogicException(\sprintf('The "worker_loop_max" runtime option must be an integer, "%s" given.', get_debug_type($workerLoopMax)));
+ }
+
+ $options['worker_loop_max'] = (int) ($workerLoopMax ?? 500);
+
parent::__construct($options);
}
public function getRunner(?object $application): RunnerInterface
{
if ($application instanceof HttpKernelInterface) {
+ if ($_SERVER['FRANKENPHP_WORKER'] ?? false) {
+ return new FrankenPhpWorkerRunner($application, $this->options['worker_loop_max']);
+ }
+
return new HttpKernelRunner($application, Request::createFromGlobals(), $this->options['debug'] ?? false);
}
@@ -162,7 +176,12 @@ public function getRunner(?object $application): RunnerInterface
if (!$application->getName() || !$console->has($application->getName())) {
$application->setName($_SERVER['argv'][0]);
- $console->add($application);
+
+ if (!method_exists($console, 'addCommand') || method_exists($console, 'add') && (new \ReflectionMethod($console, 'add'))->getDeclaringClass()->getName() !== (new \ReflectionMethod($console, 'addCommand'))->getDeclaringClass()->getName()) {
+ $console->add($application);
+ } else {
+ $console->addCommand($application);
+ }
}
$console->setDefaultCommand($application->getName(), true);
diff --git a/src/Symfony/Component/Runtime/Tests/FrankenPhpWorkerRunnerTest.php b/src/Symfony/Component/Runtime/Tests/FrankenPhpWorkerRunnerTest.php
new file mode 100644
index 0000000000000..1b5ec992953ad
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/FrankenPhpWorkerRunnerTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Tests;
+
+require_once __DIR__.'/frankenphp-function-mock.php';
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\TerminableInterface;
+use Symfony\Component\Runtime\Runner\FrankenPhpWorkerRunner;
+
+interface TestAppInterface extends HttpKernelInterface, TerminableInterface
+{
+}
+
+class FrankenPhpWorkerRunnerTest extends TestCase
+{
+ public function testRun()
+ {
+ $application = $this->createMock(TestAppInterface::class);
+ $application
+ ->expects($this->once())
+ ->method('handle')
+ ->willReturnCallback(function (Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response {
+ $this->assertSame('bar', $request->server->get('FOO'));
+
+ return new Response();
+ });
+ $application->expects($this->once())->method('terminate');
+
+ $_SERVER['FOO'] = 'bar';
+
+ $runner = new FrankenPhpWorkerRunner($application, 500);
+ $this->assertSame(0, $runner->run());
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Tests/SymfonyRuntimeTest.php b/src/Symfony/Component/Runtime/Tests/SymfonyRuntimeTest.php
new file mode 100644
index 0000000000000..c6aff2a1a7841
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/SymfonyRuntimeTest.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Runtime\Runner\FrankenPhpWorkerRunner;
+use Symfony\Component\Runtime\SymfonyRuntime;
+
+class SymfonyRuntimeTest extends TestCase
+{
+ public function testGetRunner()
+ {
+ $application = $this->createStub(HttpKernelInterface::class);
+
+ $runtime = new SymfonyRuntime();
+ $this->assertNotInstanceOf(FrankenPhpWorkerRunner::class, $runtime->getRunner(null));
+ $this->assertNotInstanceOf(FrankenPhpWorkerRunner::class, $runtime->getRunner($application));
+
+ $_SERVER['FRANKENPHP_WORKER'] = 1;
+ $this->assertInstanceOf(FrankenPhpWorkerRunner::class, $runtime->getRunner($application));
+ }
+
+ public function testStringWorkerMaxLoopThrows()
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('The "worker_loop_max" runtime option must be an integer, "string" given.');
+
+ new SymfonyRuntime(['worker_loop_max' => 'foo']);
+ }
+
+ public function testBoolWorkerMaxLoopThrows()
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('The "worker_loop_max" runtime option must be an integer, "bool" given.');
+
+ new SymfonyRuntime(['worker_loop_max' => false]);
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Tests/frankenphp-function-mock.php b/src/Symfony/Component/Runtime/Tests/frankenphp-function-mock.php
new file mode 100644
index 0000000000000..4842fbdcd95c5
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/frankenphp-function-mock.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (!function_exists('frankenphp_handle_request')) {
+ function frankenphp_handle_request(callable $callable): bool
+ {
+ $callable();
+
+ return false;
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.php b/src/Symfony/Component/Runtime/Tests/phpt/application.php
index ca2de555edfb7..b51947c2afaf1 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/application.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/application.php
@@ -25,7 +25,11 @@
});
$app = new Application();
- $app->add($command);
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($command);
+ } else {
+ $app->add($command);
+ }
$app->setDefaultCommand('go', true);
return $app;
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
index 929b4401e86b9..aa40eda627151 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
@@ -23,7 +23,11 @@
$command->setName('my_command');
[$cmd, $args] = $runtime->getResolver(require __DIR__.'/command.php')->resolve();
- $app->add($cmd(...$args));
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($cmd(...$args));
+ } else {
+ $app->add($cmd(...$args));
+ }
return $app;
};
diff --git a/src/Symfony/Component/Runtime/composer.json b/src/Symfony/Component/Runtime/composer.json
index fa9c2cb3f58d0..624f90541d30f 100644
--- a/src/Symfony/Component/Runtime/composer.json
+++ b/src/Symfony/Component/Runtime/composer.json
@@ -21,10 +21,10 @@
},
"require-dev": {
"composer/composer": "^2.6",
- "symfony/console": "^6.4|^7.0",
- "symfony/dotenv": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0"
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dotenv": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/dotenv": "<6.4"
diff --git a/src/Symfony/Component/Scheduler/CHANGELOG.md b/src/Symfony/Component/Scheduler/CHANGELOG.md
index 67512476a7a8e..26067e3589104 100644
--- a/src/Symfony/Component/Scheduler/CHANGELOG.md
+++ b/src/Symfony/Component/Scheduler/CHANGELOG.md
@@ -5,6 +5,7 @@ CHANGELOG
---
* Add `TriggerNormalizer`
+ * Throw exception when multiple schedule provider services are registered under the same scheduler name
7.2
---
diff --git a/src/Symfony/Component/Scheduler/DependencyInjection/AddScheduleMessengerPass.php b/src/Symfony/Component/Scheduler/DependencyInjection/AddScheduleMessengerPass.php
index 696422e0d28da..64880149244e1 100644
--- a/src/Symfony/Component/Scheduler/DependencyInjection/AddScheduleMessengerPass.php
+++ b/src/Symfony/Component/Scheduler/DependencyInjection/AddScheduleMessengerPass.php
@@ -46,6 +46,11 @@ public function process(ContainerBuilder $container): void
$scheduleProviderIds = [];
foreach ($container->findTaggedServiceIds('scheduler.schedule_provider') as $serviceId => $tags) {
$name = $tags[0]['name'];
+
+ if (isset($scheduleProviderIds[$name])) {
+ throw new InvalidArgumentException(\sprintf('Schedule provider service "%s" can not replace already registered service "%s" for schedule "%s". Make sure to register only one provider per schedule name.', $serviceId, $scheduleProviderIds[$name], $name), 1);
+ }
+
$scheduleProviderIds[$name] = $serviceId;
}
diff --git a/src/Symfony/Component/Scheduler/Tests/DependencyInjection/RegisterProviderTest.php b/src/Symfony/Component/Scheduler/Tests/DependencyInjection/RegisterProviderTest.php
new file mode 100644
index 0000000000000..8bbfe41f71be4
--- /dev/null
+++ b/src/Symfony/Component/Scheduler/Tests/DependencyInjection/RegisterProviderTest.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Scheduler\Tests\DependencyInjection;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
+use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
+use Symfony\Component\Scheduler\Tests\Fixtures\SomeScheduleProvider;
+
+class RegisterProviderTest extends TestCase
+{
+ public function testErrorOnMultipleProvidersForTheSameSchedule()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionCode(1);
+
+ $container = new ContainerBuilder();
+
+ $container->register('provider_a', SomeScheduleProvider::class)->addTag('scheduler.schedule_provider', ['name' => 'default']);
+ $container->register('provider_b', SomeScheduleProvider::class)->addTag('scheduler.schedule_provider', ['name' => 'default']);
+
+ (new AddScheduleMessengerPass())->process($container);
+ }
+}
diff --git a/src/Symfony/Component/Scheduler/composer.json b/src/Symfony/Component/Scheduler/composer.json
index e907a79d55dcb..8a5cc60506212 100644
--- a/src/Symfony/Component/Scheduler/composer.json
+++ b/src/Symfony/Component/Scheduler/composer.json
@@ -21,17 +21,17 @@
],
"require": {
"php": ">=8.2",
- "symfony/clock": "^6.4|^7.0"
+ "symfony/clock": "^6.4|^7.0|^8.0"
},
"require-dev": {
"dragonmantank/cron-expression": "^3.1",
- "symfony/cache": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/lock": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.1"
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.1|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Scheduler\\": "" },
diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
index b2e18a29efe51..683e46d4e0eb8 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
@@ -32,16 +32,12 @@ abstract class AbstractToken implements TokenInterface, \Serializable
*/
public function __construct(array $roles = [])
{
- $this->roleNames = [];
-
- foreach ($roles as $role) {
- $this->roleNames[] = (string) $role;
- }
+ $this->roleNames = $roles;
}
public function getRoleNames(): array
{
- return $this->roleNames ??= self::__construct($this->user->getRoles()) ?? $this->roleNames;
+ return $this->roleNames ??= $this->user?->getRoles() ?? [];
}
public function getUserIdentifier(): string
@@ -90,13 +86,7 @@ public function eraseCredentials(): void
*/
public function __serialize(): array
{
- $data = [$this->user, true, null, $this->attributes];
-
- if (!$this->user instanceof EquatableInterface) {
- $data[] = $this->roleNames;
- }
-
- return $data;
+ return [$this->user, true, null, $this->attributes, $this->getRoleNames()];
}
/**
@@ -160,12 +150,7 @@ public function __toString(): string
$class = static::class;
$class = substr($class, strrpos($class, '\\') + 1);
- $roles = [];
- foreach ($this->roleNames as $role) {
- $roles[] = $role;
- }
-
- return \sprintf('%s(user="%s", roles="%s")', $class, $this->getUserIdentifier(), implode(', ', $roles));
+ return \sprintf('%s(user="%s", roles="%s")', $class, $this->getUserIdentifier(), implode(', ', $this->getRoleNames()));
}
/**
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
index 1403aaaaf0b15..3ab6b92c1d956 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
@@ -45,11 +45,10 @@ public function __construct(
*/
public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int
{
- $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote();
- $vote ??= new Vote();
+ $vote = 3 < \func_num_args() ? func_get_arg(3) : null;
if ($attributes === [self::PUBLIC_ACCESS]) {
- $vote->reasons[] = 'Access is public.';
+ $vote?->addReason('Access is public.');
return VoterInterface::ACCESS_GRANTED;
}
@@ -73,7 +72,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
if ((self::IS_AUTHENTICATED_FULLY === $attribute || self::IS_AUTHENTICATED_REMEMBERED === $attribute)
&& $this->authenticationTrustResolver->isFullFledged($token)
) {
- $vote->reasons[] = 'The user is fully authenticated.';
+ $vote?->addReason('The user is fully authenticated.');
return VoterInterface::ACCESS_GRANTED;
}
@@ -81,32 +80,32 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
if (self::IS_AUTHENTICATED_REMEMBERED === $attribute
&& $this->authenticationTrustResolver->isRememberMe($token)
) {
- $vote->reasons[] = 'The user is remembered.';
+ $vote?->addReason('The user is remembered.');
return VoterInterface::ACCESS_GRANTED;
}
if (self::IS_AUTHENTICATED === $attribute && $this->authenticationTrustResolver->isAuthenticated($token)) {
- $vote->reasons[] = 'The user is authenticated.';
+ $vote?->addReason('The user is authenticated.');
return VoterInterface::ACCESS_GRANTED;
}
if (self::IS_REMEMBERED === $attribute && $this->authenticationTrustResolver->isRememberMe($token)) {
- $vote->reasons[] = 'The user is remembered.';
+ $vote?->addReason('The user is remembered.');
return VoterInterface::ACCESS_GRANTED;
}
if (self::IS_IMPERSONATOR === $attribute && $token instanceof SwitchUserToken) {
- $vote->reasons[] = 'The user is impersonating another user.';
+ $vote?->addReason('The user is impersonating another user.');
return VoterInterface::ACCESS_GRANTED;
}
}
if (VoterInterface::ACCESS_DENIED === $result) {
- $vote->reasons[] = 'The user is not appropriately authenticated.';
+ $vote?->addReason('The user is not appropriately authenticated.');
}
return $result;
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php
index 03a9f7571a571..4fb5502fd91c5 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php
@@ -42,7 +42,6 @@ public function supportsType(string $subjectType): bool
public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int
{
- $vote ??= new Vote();
$context = new IsGrantedContext($token, $token->getUser(), $this->authorizationChecker);
$failingClosures = [];
$result = VoterInterface::ACCESS_ABSTAIN;
@@ -54,7 +53,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes, ?
$name = (new \ReflectionFunction($attribute))->name;
$result = VoterInterface::ACCESS_DENIED;
if ($attribute($context, $subject)) {
- $vote->reasons[] = \sprintf('Closure %s returned true.', $name);
+ $vote?->addReason(\sprintf('Closure %s returned true.', $name));
return VoterInterface::ACCESS_GRANTED;
}
@@ -63,7 +62,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes, ?
}
if ($failingClosures) {
- $vote->reasons[] = \sprintf('Closure%s %s returned false.', 1 < \count($failingClosures) ? 's' : '', implode(', ', $failingClosures));
+ $vote?->addReason(\sprintf('Closure%s %s returned false.', 1 < \count($failingClosures) ? 's' : '', implode(', ', $failingClosures)));
}
return $result;
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
index 35d727a8eb15e..719aae7d46872 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
@@ -49,8 +49,7 @@ public function supportsType(string $subjectType): bool
*/
public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int
{
- $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote();
- $vote ??= new Vote();
+ $vote = 3 < \func_num_args() ? func_get_arg(3) : null;
$result = VoterInterface::ACCESS_ABSTAIN;
$variables = null;
$failingExpressions = [];
@@ -64,7 +63,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
$result = VoterInterface::ACCESS_DENIED;
if ($this->expressionLanguage->evaluate($attribute, $variables)) {
- $vote->reasons[] = \sprintf('Expression (%s) is true.', $attribute);
+ $vote?->addReason(\sprintf('Expression (%s) is true.', $attribute));
return VoterInterface::ACCESS_GRANTED;
}
@@ -73,7 +72,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
}
if ($failingExpressions) {
- $vote->reasons[] = \sprintf('Expression (%s) is false.', implode(') || (', $failingExpressions));
+ $vote?->addReason(\sprintf('Expression (%s) is false.', implode(') || (', $failingExpressions)));
}
return $result;
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
index 46c08d15b48ed..2225e8d4d4c41 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
@@ -30,8 +30,7 @@ public function __construct(
*/
public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int
{
- $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote();
- $vote ??= new Vote();
+ $vote = 3 < \func_num_args() ? func_get_arg(3) : null;
$result = VoterInterface::ACCESS_ABSTAIN;
$roles = $this->extractRoles($token);
$missingRoles = [];
@@ -44,7 +43,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
$result = VoterInterface::ACCESS_DENIED;
if (\in_array($attribute, $roles, true)) {
- $vote->reasons[] = \sprintf('The user has %s.', $attribute);
+ $vote?->addReason(\sprintf('The user has %s.', $attribute));
return VoterInterface::ACCESS_GRANTED;
}
@@ -53,7 +52,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
}
if (VoterInterface::ACCESS_DENIED === $result) {
- $vote->reasons[] = \sprintf('The user doesn\'t have%s %s.', 1 < \count($missingRoles) ? ' any of' : '', implode(', ', $missingRoles));
+ $vote?->addReason(\sprintf('The user doesn\'t have%s %s.', 1 < \count($missingRoles) ? ' any of' : '', implode(', ', $missingRoles)));
}
return $result;
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
index 47572797ee906..ec92606359859 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
@@ -32,9 +32,9 @@ public function __construct(
public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int
{
- $result = $this->voter->vote($token, $subject, $attributes, $vote ??= new Vote());
+ $result = $this->voter->vote($token, $subject, $attributes, $vote);
- $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result, $vote->reasons), 'debug.security.authorization.vote');
+ $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result, $vote->reasons ?? []), 'debug.security.authorization.vote');
return $result;
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
index 3d7fd9e2d7a1f..55930def8fda9 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
@@ -29,10 +29,9 @@ abstract class Voter implements VoterInterface, CacheableVoterInterface
*/
public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int
{
- $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote();
- $vote ??= new Vote();
+ $vote = 3 < \func_num_args() ? func_get_arg(3) : null;
// abstain vote by default in case none of the attributes are supported
- $vote->result = self::ACCESS_ABSTAIN;
+ $voteResult = self::ACCESS_ABSTAIN;
foreach ($attributes as $attribute) {
try {
@@ -48,15 +47,27 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/*
}
// as soon as at least one attribute is supported, default is to deny access
- $vote->result = self::ACCESS_DENIED;
+ $voteResult = self::ACCESS_DENIED;
+
+ if (null !== $vote) {
+ $vote->result = $voteResult;
+ }
if ($this->voteOnAttribute($attribute, $subject, $token, $vote)) {
// grant access as soon as at least one attribute returns a positive response
- return $vote->result = self::ACCESS_GRANTED;
+ if (null !== $vote) {
+ $vote->result = self::ACCESS_GRANTED;
+ }
+
+ return self::ACCESS_GRANTED;
}
}
- return $vote->result;
+ if (null !== $vote) {
+ $vote->result = $voteResult;
+ }
+
+ return $voteResult;
}
/**
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
index a8f87e09da7e6..eaada3061dbfe 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
@@ -33,35 +33,51 @@ public static function getTests(): array
return [
[$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
+ [$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access', new Vote()],
[$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'],
+ [$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access', new Vote()],
[$voter, ['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access'],
+ [$voter, ['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access', new Vote()],
[$voter, ['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access'],
+ [$voter, ['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access', new Vote()],
[$voter, ['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access'],
+ [$voter, ['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access', new Vote()],
[$voter, ['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported'],
+ [$voter, ['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported', new Vote()],
[$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, new class {}, 'ACCESS_ABSTAIN if class is not supported'],
+ [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, new class {}, 'ACCESS_ABSTAIN if class is not supported', new Vote()],
[$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null'],
+ [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null', new Vote()],
[$voter, [], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided'],
+ [$voter, [], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided', new Vote()],
[$voter, [new StringableAttribute()], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
+ [$voter, [new StringableAttribute()], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access', new Vote()],
[$voter, [new \stdClass()], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings'],
+ [$voter, [new \stdClass()], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings', new Vote()],
[$integerVoter, [42], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute is an integer'],
+ [$integerVoter, [42], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute is an integer', new Vote()],
];
}
/**
* @dataProvider getTests
*/
- public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message)
+ public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message, ?Vote $vote = null)
{
- $this->assertEquals($expectedVote, $voter->vote($this->token, $object, $attributes), $message);
+ $this->assertSame($expectedVote, $voter->vote($this->token, $object, $attributes, $vote), $message);
+
+ if (null !== $vote) {
+ self::assertSame($expectedVote, $vote->result);
+ }
}
public function testVoteWithTypeError()
diff --git a/src/Symfony/Component/Security/Core/User/UserInterface.php b/src/Symfony/Component/Security/Core/User/UserInterface.php
index 120521211b326..24c0581f83cbd 100644
--- a/src/Symfony/Component/Security/Core/User/UserInterface.php
+++ b/src/Symfony/Component/Security/Core/User/UserInterface.php
@@ -15,9 +15,7 @@
* Represents the interface that all user classes must implement.
*
* This interface is useful because the authentication layer can deal with
- * the object through its lifecycle, using the object to get the hashed
- * password (for checking against a submitted password), assigning roles
- * and so on.
+ * the object through its lifecycle, assigning roles and so on.
*
* Regardless of how your users are loaded or where they come from (a database,
* configuration, web service, etc.), you will have a class that implements
diff --git a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
index e6741a48f1945..b92be87e6ef89 100644
--- a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
+++ b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Security\Core\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
@@ -25,6 +26,7 @@ class UserPassword extends Constraint
public string $message = 'This value should be the user\'s current password.';
public string $service = 'security.validator.user_password';
+ #[HasNamedArguments]
public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $groups = null, mixed $payload = null)
{
parent::__construct($options, $groups, $payload);
diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json
index 0aaff1e3645bf..9d662f7e9eeda 100644
--- a/src/Symfony/Component/Security/Core/composer.json
+++ b/src/Symfony/Component/Security/Core/composer.json
@@ -20,20 +20,20 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/event-dispatcher-contracts": "^2.5|^3",
"symfony/service-contracts": "^2.5|^3",
- "symfony/password-hasher": "^6.4|^7.0"
+ "symfony/password-hasher": "^6.4|^7.0|^8.0"
},
"require-dev": {
"psr/container": "^1.1|^2.0",
"psr/cache": "^1.0|^2.0|^3.0",
- "symfony/cache": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/ldap": "^6.4|^7.0",
- "symfony/string": "^6.4|^7.0",
- "symfony/translation": "^6.4.3|^7.0.3",
- "symfony/validator": "^6.4|^7.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/ldap": "^6.4|^7.0|^8.0",
+ "symfony/string": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4.3|^7.0.3|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
diff --git a/src/Symfony/Component/Security/Csrf/composer.json b/src/Symfony/Component/Security/Csrf/composer.json
index c2bfed1de3d7e..6129d76ce8e1b 100644
--- a/src/Symfony/Component/Security/Csrf/composer.json
+++ b/src/Symfony/Component/Security/Csrf/composer.json
@@ -17,12 +17,12 @@
],
"require": {
"php": ">=8.2",
- "symfony/security-core": "^6.4|^7.0"
+ "symfony/security-core": "^6.4|^7.0|^8.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0"
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md
index 275180ff87b3b..6c485dc6e5450 100644
--- a/src/Symfony/Component/Security/Http/CHANGELOG.md
+++ b/src/Symfony/Component/Security/Http/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+7.4
+---
+
+ * Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
+ * Deprecate `AbstractListener::__invoke`
+
7.3
---
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 6c256dba60955..d3e157f2a9c15 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -16,6 +16,7 @@
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -122,7 +123,13 @@ public static function getSubscribedEvents()
protected function callListeners(RequestEvent $event, iterable $listeners)
{
foreach ($listeners as $listener) {
- $listener($event);
+ if (!$listener instanceof FirewallListenerInterface) {
+ trigger_deprecation('symfony/security-http', '7.4', 'Using a callable as firewall listener is deprecated, extend "%s" or implement "%s" instead.', AbstractListener::class, FirewallListenerInterface::class);
+
+ $listener($event);
+ } elseif (false !== $listener->supports($event->getRequest())) {
+ $listener->authenticate($event);
+ }
if ($event->hasResponse()) {
break;
@@ -130,8 +137,8 @@ protected function callListeners(RequestEvent $event, iterable $listeners)
}
}
- private function getListenerPriority(object $logoutListener): int
+ private function getListenerPriority(object $listener): int
{
- return $logoutListener instanceof FirewallListenerInterface ? $logoutListener->getPriority() : 0;
+ return $listener instanceof FirewallListenerInterface ? $listener->getPriority() : 0;
}
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php
index b5349e5e552cc..b30614defd215 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php
@@ -20,8 +20,13 @@
*/
abstract class AbstractListener implements FirewallListenerInterface
{
+ /**
+ * @deprecated since Symfony 7.4, to be removed in 8.0
+ */
final public function __invoke(RequestEvent $event): void
{
+ trigger_deprecation('symfony/security-http', '7.4', 'The "%s()" method is deprecated since Symfony 7.4 and will be removed in 8.0.', __METHOD__);
+
if (false !== $this->supports($event->getRequest())) {
$this->authenticate($event);
}
diff --git a/src/Symfony/Component/Security/Http/FirewallMap.php b/src/Symfony/Component/Security/Http/FirewallMap.php
index 3b01cbdc161a6..444f71ceebbda 100644
--- a/src/Symfony/Component/Security/Http/FirewallMap.php
+++ b/src/Symfony/Component/Security/Http/FirewallMap.php
@@ -14,6 +14,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -25,12 +26,12 @@
class FirewallMap implements FirewallMapInterface
{
/**
- * @var list, ExceptionListener|null, LogoutListener|null}>
+ * @var list, ExceptionListener|null, LogoutListener|null}>
*/
private array $map = [];
/**
- * @param list $listeners
+ * @param list $listeners
*/
public function add(?RequestMatcherInterface $requestMatcher = null, array $listeners = [], ?ExceptionListener $exceptionListener = null, ?LogoutListener $logoutListener = null): void
{
diff --git a/src/Symfony/Component/Security/Http/FirewallMapInterface.php b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
index fa43d6a6e9ba3..1925d3dec23a0 100644
--- a/src/Symfony/Component/Security/Http/FirewallMapInterface.php
+++ b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -35,7 +36,7 @@ interface FirewallMapInterface
* If there is no logout listener, the third element of the outer array
* must be null.
*
- * @return array{iterable, ExceptionListener, LogoutListener}
+ * @return array{iterable, ExceptionListener, LogoutListener}
*/
public function getListeners(Request $request): array;
}
diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php
index 73494f405468c..d34b31f2bdeb8 100644
--- a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php
@@ -232,7 +232,7 @@ protected function supports(string $attribute, mixed $subject): bool
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
- $vote->reasons[] = 'Because I can 😈.';
+ $vote?->addReason('Because I can 😈.');
return false;
}
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
index 83df93d36169f..82ecbcb88b1a2 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
@@ -68,7 +68,8 @@ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
$this->expectException(AccessDeniedException::class);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
@@ -95,7 +96,8 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
$accessMap
);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertNull($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testHandleWhenAccessMapReturnsEmptyAttributes()
@@ -124,7 +126,8 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
- $listener(new LazyResponseEvent($event));
+ $this->assertNull($listener->supports($request));
+ $listener->authenticate(new LazyResponseEvent($event));
}
public function testHandleWhenTheSecurityTokenStorageHasNoToken()
@@ -154,7 +157,8 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
$this->expectException(AccessDeniedException::class);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testHandleWhenPublicAccessIsAllowed()
@@ -182,7 +186,8 @@ public function testHandleWhenPublicAccessIsAllowed()
false
);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertNull($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testHandleWhenPublicAccessWhileAuthenticated()
@@ -212,7 +217,8 @@ public function testHandleWhenPublicAccessWhileAuthenticated()
false
);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertNull($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
@@ -246,7 +252,8 @@ public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
$accessMap
);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testLazyPublicPagesShouldNotAccessTokenStorage()
@@ -263,7 +270,9 @@ public function testLazyPublicPagesShouldNotAccessTokenStorage()
;
$listener = new AccessListener($tokenStorage, $this->createMock(AccessDecisionManagerInterface::class), $accessMap, false);
- $listener(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)));
+
+ $this->assertNull($listener->supports($request));
+ $listener->authenticate(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)));
}
public function testConstructWithTrueExceptionOnNoToken()
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php
index 06c4c6d0e3422..5a4be3feb1eae 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php
@@ -39,12 +39,8 @@ public function testHandleWithNotSecuredRequestAndHttpChannel()
->willReturn([[], 'http'])
;
- $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
-
$listener = new ChannelListener($accessMap);
- $listener($event);
-
- $this->assertNull($event->getResponse());
+ $this->assertFalse($listener->supports($request));
}
public function testHandleWithSecuredRequestAndHttpsChannel()
@@ -64,12 +60,8 @@ public function testHandleWithSecuredRequestAndHttpsChannel()
->willReturn([[], 'https'])
;
- $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
-
$listener = new ChannelListener($accessMap);
- $listener($event);
-
- $this->assertNull($event->getResponse());
+ $this->assertFalse($listener->supports($request));
}
public function testHandleWithNotSecuredRequestAndHttpsChannel()
@@ -92,7 +84,9 @@ public function testHandleWithNotSecuredRequestAndHttpsChannel()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
$listener = new ChannelListener($accessMap);
- $listener($event);
+ $this->assertTrue($listener->supports($request));
+
+ $listener->authenticate($event);
$response = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $response);
@@ -119,7 +113,9 @@ public function testHandleWithSecuredRequestAndHttpChannel()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
$listener = new ChannelListener($accessMap);
- $listener($event);
+ $this->assertTrue($listener->supports($request));
+
+ $listener->authenticate($event);
$response = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $response);
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
index 585fca8af10ff..03d45722822b5 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
@@ -179,7 +179,7 @@ public function testInvalidTokenInSession($token)
->with(null);
$listener = new ContextListener($tokenStorage, [], 'key123');
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public static function provideInvalidToken()
@@ -203,7 +203,7 @@ public function testHandleAddsKernelResponseListener()
->method('addListener')
->with(KernelEvents::RESPONSE, $listener->onKernelResponse(...));
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST));
}
public function testOnKernelResponseListenerRemovesItself()
@@ -236,7 +236,7 @@ public function testHandleRemovesTokenIfNoPreviousSessionWasFound()
$tokenStorage->expects($this->once())->method('setToken')->with(null);
$listener = new ContextListener($tokenStorage, [], 'key123');
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public function testIfTokenIsDeauthenticated()
@@ -262,7 +262,7 @@ public function testTokenIsNotDeauthenticatedOnUserChangeIfNotAnInstanceOfAbstra
$request->cookies->set('MOCKSESSID', true);
$listener = new ContextListener($tokenStorage, [new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], 'context_key');
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertInstanceOf(CustomToken::class, $tokenStorage->getToken());
$this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser());
@@ -270,7 +270,6 @@ public function testTokenIsNotDeauthenticatedOnUserChangeIfNotAnInstanceOfAbstra
public function testIfTokenIsNotDeauthenticated()
{
- $tokenStorage = new TokenStorage();
$badRefreshedUser = new InMemoryUser('foobar', 'baz');
$goodRefreshedUser = new InMemoryUser('foobar', 'bar');
$tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser);
@@ -326,7 +325,7 @@ public function testWithPreviousNotStartedSession()
$tokenStorage = new TokenStorage();
$listener = new ContextListener($tokenStorage, [], 'context_key', null, null, null, $tokenStorage->getToken(...));
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertSame($usageIndex, $session->getUsageIndex());
}
@@ -348,7 +347,7 @@ public function testSessionIsNotReported()
$tokenStorage = new TokenStorage();
$listener = new ContextListener($tokenStorage, [], 'context_key', null, null, null, $tokenStorage->getToken(...));
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$listener->onKernelResponse(new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response()));
}
@@ -370,7 +369,7 @@ public function testOnKernelResponseRemoveListener()
$listener = new ContextListener($tokenStorage, [], 'session', null, $dispatcher, null, $tokenStorage->getToken(...));
$this->assertSame([], $dispatcher->getListeners());
- $listener(new RequestEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertNotEmpty($dispatcher->getListeners());
$listener->onKernelResponse(new ResponseEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response()));
@@ -468,7 +467,7 @@ private function handleEventWithPreviousSession($userProviders, ?UserInterface $
$listener = new ContextListener($tokenStorage, $userProviders, 'context_key', null, null, null, $sessionTrackerEnabler);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
if (null !== $user) {
++$usageIndex;
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php
index c7cdc7abd216a..acdeccfb5e11f 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php
@@ -29,13 +29,7 @@ class LogoutListenerTest extends TestCase
{
public function testHandleUnmatchedPath()
{
- $dispatcher = $this->getEventDispatcher();
- [$listener, , $httpUtils, $options] = $this->getListener($dispatcher);
-
- $logoutEventDispatched = false;
- $dispatcher->addListener(LogoutEvent::class, function () use (&$logoutEventDispatched) {
- $logoutEventDispatched = true;
- });
+ [$listener, , $httpUtils, $options] = $this->getListener();
$request = new Request();
@@ -44,9 +38,7 @@ public function testHandleUnmatchedPath()
->with($request, $options['logout_path'])
->willReturn(false);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
-
- $this->assertFalse($logoutEventDispatched, 'LogoutEvent should not have been dispatched.');
+ $this->assertFalse($listener->supports($request));
}
public function testHandleMatchedPathWithCsrfValidation()
@@ -75,7 +67,7 @@ public function testHandleMatchedPathWithCsrfValidation()
$tokenStorage->expects($this->once())
->method('getToken')
- ->willReturn($token = $this->getToken());
+ ->willReturn($this->getToken());
$tokenStorage->expects($this->once())
->method('setToken')
@@ -83,7 +75,8 @@ public function testHandleMatchedPathWithCsrfValidation()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
- $listener($event);
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate($event);
$this->assertSame($response, $event->getResponse());
}
@@ -107,7 +100,7 @@ public function testHandleMatchedPathWithoutCsrfValidation()
$tokenStorage->expects($this->once())
->method('getToken')
- ->willReturn($token = $this->getToken());
+ ->willReturn($this->getToken());
$tokenStorage->expects($this->once())
->method('setToken')
@@ -115,7 +108,8 @@ public function testHandleMatchedPathWithoutCsrfValidation()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
- $listener($event);
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate($event);
$this->assertSame($response, $event->getResponse());
}
@@ -133,7 +127,8 @@ public function testNoResponseSet()
$this->expectException(\RuntimeException::class);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
/**
@@ -161,7 +156,8 @@ public function testCsrfValidationFails($invalidToken)
$this->expectException(LogoutException::class);
- $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ $this->assertTrue($listener->supports($request));
+ $listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}
public static function provideInvalidCsrfTokens(): array
@@ -188,7 +184,7 @@ private function getHttpUtils()
return $this->createMock(HttpUtils::class);
}
- private function getListener($eventDispatcher = null, $tokenManager = null)
+ private function getListener($eventDispatcher = null, $tokenManager = null): array
{
$listener = new LogoutListener(
$tokenStorage = $this->getTokenStorage(),
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
index 114d0db979e46..0c012ab338db7 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
@@ -61,10 +61,7 @@ public function testFirewallNameIsRequired()
public function testEventIsIgnoredIfUsernameIsNotPassedWithTheRequest()
{
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
- $listener($this->event);
-
- $this->assertNull($this->event->getResponse());
- $this->assertNull($this->tokenStorage->getToken());
+ $this->assertFalse($listener->supports($this->event->getRequest()));
}
public function testExitUserThrowsAuthenticationExceptionIfNoCurrentToken()
@@ -75,7 +72,8 @@ public function testExitUserThrowsAuthenticationExceptionIfNoCurrentToken()
$this->expectException(AuthenticationCredentialsNotFoundException::class);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testExitUserThrowsAuthenticationExceptionIfOriginalTokenCannotBeFound()
@@ -89,7 +87,8 @@ public function testExitUserThrowsAuthenticationExceptionIfOriginalTokenCannotBe
$this->expectException(AuthenticationCredentialsNotFoundException::class);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testExitUserUpdatesToken()
@@ -100,7 +99,8 @@ public function testExitUserUpdatesToken()
$this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE);
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame([], $this->request->query->all());
$this->assertSame('', $this->request->server->get('QUERY_STRING'));
@@ -134,7 +134,8 @@ public function testExitUserDispatchesEventWithRefreshedUser()
;
$listener = new SwitchUserListener($this->tokenStorage, $userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testSwitchUserIsDisallowed()
@@ -153,7 +154,8 @@ public function testSwitchUserIsDisallowed()
$this->expectException(AccessDeniedException::class);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testSwitchUserTurnsAuthenticationExceptionTo403()
@@ -170,7 +172,8 @@ public function testSwitchUserTurnsAuthenticationExceptionTo403()
$this->expectException(AccessDeniedException::class);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testSwitchUser()
@@ -188,7 +191,8 @@ public function testSwitchUser()
->method('checkPostAuth')->with($this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()), $token);
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame([], $this->request->query->all());
$this->assertSame('', $this->request->server->get('QUERY_STRING'));
@@ -217,7 +221,8 @@ public function testSwitchUserAlreadySwitched()
->method('checkPostAuth')->with($targetsUser);
$listener = new SwitchUserListener($tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, false);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame([], $this->request->query->all());
$this->assertSame('', $this->request->server->get('QUERY_STRING'));
@@ -243,7 +248,8 @@ public function testSwitchUserWorksWithFalsyUsernames()
->method('checkPostAuth')->with($this->callback(fn ($argUser) => $user->isEqualTo($argUser)));
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame([], $this->request->query->all());
$this->assertSame('', $this->request->server->get('QUERY_STRING'));
@@ -270,7 +276,8 @@ public function testSwitchUserKeepsOtherQueryStringParameters()
->method('checkPostAuth')->with($targetsUser);
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame('page=3§ion=2', $this->request->server->get('QUERY_STRING'));
$this->assertInstanceOf(UsernamePasswordToken::class, $this->tokenStorage->getToken());
@@ -308,7 +315,8 @@ public function testSwitchUserWithReplacedToken()
);
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertSame($replacedToken, $this->tokenStorage->getToken());
}
@@ -321,7 +329,8 @@ public function testSwitchUserThrowsAuthenticationExceptionIfNoCurrentToken()
$this->expectException(AuthenticationCredentialsNotFoundException::class);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
public function testSwitchUserStateless()
@@ -340,7 +349,8 @@ public function testSwitchUserStateless()
->method('checkPostAuth')->with($targetsUser);
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, true);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
$this->assertInstanceOf(UsernamePasswordToken::class, $this->tokenStorage->getToken());
$this->assertFalse($this->event->hasResponse());
@@ -371,6 +381,7 @@ public function testSwitchUserRefreshesOriginalToken()
;
$listener = new SwitchUserListener($this->tokenStorage, $userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher);
- $listener($this->event);
+ $this->assertTrue($listener->supports($this->event->getRequest()));
+ $listener->authenticate($this->event);
}
}
diff --git a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
index f9417d237433c..bfa9bebdd0b32 100644
--- a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
@@ -12,17 +12,22 @@
namespace Symfony\Component\Security\Http\Tests;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Http\Firewall;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
class FirewallTest extends TestCase
{
+ use ExpectUserDeprecationMessageTrait;
+
public function testOnKernelRequestRegistersExceptionListener()
{
$dispatcher = $this->createMock(EventDispatcherInterface::class);
@@ -52,21 +57,25 @@ public function testOnKernelRequestRegistersExceptionListener()
public function testOnKernelRequestStopsWhenThereIsAResponse()
{
- $called = [];
-
- $first = function () use (&$called) {
- $called[] = 1;
- };
-
- $second = function () use (&$called) {
- $called[] = 2;
+ $listener = new class extends AbstractListener {
+ public int $callCount = 0;
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ ++$this->callCount;
+ }
};
$map = $this->createMock(FirewallMapInterface::class);
$map
->expects($this->once())
->method('getListeners')
- ->willReturn([[$first, $second], null, null])
+ ->willReturn([[$listener, $listener], null, null])
;
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST);
@@ -75,7 +84,7 @@ public function testOnKernelRequestStopsWhenThereIsAResponse()
$firewall = new Firewall($map, $this->createMock(EventDispatcherInterface::class));
$firewall->onKernelRequest($event);
- $this->assertSame([1], $called);
+ $this->assertSame(1, $listener->callCount);
}
public function testOnKernelRequestWithSubRequest()
@@ -97,4 +106,87 @@ public function testOnKernelRequestWithSubRequest()
$this->assertFalse($event->hasResponse());
}
+
+ public function testFirewallListenersAreCalled()
+ {
+ $calledListeners = [];
+
+ $firewallListener = new class($calledListeners) implements FirewallListenerInterface {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'firewallListener';
+ }
+
+ public static function getPriority(): int
+ {
+ return 0;
+ }
+ };
+ $callableFirewallListener = new class($calledListeners) extends AbstractListener {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'callableFirewallListener';
+ }
+ };
+
+ $request = $this->createMock(Request::class);
+
+ $map = $this->createMock(FirewallMapInterface::class);
+ $map
+ ->expects($this->once())
+ ->method('getListeners')
+ ->with($this->equalTo($request))
+ ->willReturn([[$firewallListener, $callableFirewallListener], null, null])
+ ;
+
+ $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
+
+ $firewall = new Firewall($map, $this->createMock(EventDispatcherInterface::class));
+ $firewall->onKernelRequest($event);
+
+ $this->assertSame(['firewallListener', 'callableFirewallListener'], $calledListeners);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testCallableListenersAreCalled()
+ {
+ $calledListeners = [];
+
+ $callableListener = static function() use(&$calledListeners) { $calledListeners[] = 'callableListener'; };
+
+ $request = $this->createMock(Request::class);
+
+ $map = $this->createMock(FirewallMapInterface::class);
+ $map
+ ->expects($this->once())
+ ->method('getListeners')
+ ->with($this->equalTo($request))
+ ->willReturn([[$callableListener], null, null])
+ ;
+
+ $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
+
+ $firewall = new Firewall($map, $this->createMock(EventDispatcherInterface::class));
+
+ $this->expectUserDeprecationMessage('Since symfony/security-http 7.4: Using a callable as firewall listener is deprecated, extend "Symfony\Component\Security\Http\Firewall\AbstractListener" or implement "Symfony\Component\Security\Http\Firewall\FirewallListenerInterface" instead.');
+ $firewall->onKernelRequest($event);
+
+ $this->assertSame(['callableListener'], $calledListeners);
+ }
}
diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json
index 77f6af87395ec..2d5ed369a7f57 100644
--- a/src/Symfony/Component/Security/Http/composer.json
+++ b/src/Symfony/Component/Security/Http/composer.json
@@ -18,29 +18,29 @@
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/polyfill-mbstring": "~1.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/security-core": "^7.3",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/security-core": "^7.3|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/cache": "^6.4|^7.0",
- "symfony/clock": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^3.0",
- "symfony/rate-limiter": "^6.4|^7.0",
- "symfony/routing": "^6.4|^7.0",
- "symfony/security-csrf": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/security-csrf": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3",
"web-token/jwt-library": "^3.3.2|^4.0"
},
"conflict": {
"symfony/clock": "<6.4",
- "symfony/event-dispatcher": "<6.4",
"symfony/http-client-contracts": "<3.0",
"symfony/security-bundle": "<6.4",
"symfony/security-csrf": "<6.4"
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
index bd4f505f8acf9..60cb2ec495b67 100644
--- a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
+++ b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
@@ -171,8 +171,8 @@ private function getCaller(string $method, string $interface): array
&& $method === $trace[$i]['function']
&& is_a($trace[$i]['class'], $interface, true)
) {
- $file = $trace[$i]['file'];
- $line = $trace[$i]['line'];
+ $file = $trace[$i]['file'] ?? $trace[$i + 1]['file'];
+ $line = $trace[$i]['line'] ?? $trace[$i + 1]['line'];
break;
}
diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
index 994f61246a342..432ff9b189c75 100644
--- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
+++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php
@@ -177,8 +177,11 @@ private function configureNamedSerializers(ContainerBuilder $container, ?string
$container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']);
$container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer');
+ $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName);
$container->registerAliasForArgument($serializerId, NormalizerInterface::class, $serializerName.'.normalizer');
+ $container->registerAliasForArgument($serializerId, NormalizerInterface::class, $serializerName);
$container->registerAliasForArgument($serializerId, DenormalizerInterface::class, $serializerName.'.denormalizer');
+ $container->registerAliasForArgument($serializerId, DenormalizerInterface::class, $serializerName);
$this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName);
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
index c346aafa8f450..738dfaccf3b9e 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
@@ -26,6 +26,7 @@
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
+use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -1052,7 +1053,7 @@ private function updateData(array $data, string $attribute, mixed $attributeValu
*/
private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
{
- if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false)
+ if (!($context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false)
|| !isset($attributesMetadata[$attribute]) || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth()
) {
return false;
@@ -1093,6 +1094,30 @@ protected function createChildContext(array $parentContext, string $attribute, ?
return $context;
}
+ protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
+ {
+ if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
+ return false;
+ }
+
+ if (null !== $this->classDiscriminatorResolver) {
+ $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
+ $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
+ }
+
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
+ $attributes = [];
+ foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
+ $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
+ }
+ $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
+ }
+ }
+
+ return $allowedAttributes;
+ }
+
/**
* Builds the cache key for the attributes cache.
*
diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
index 23ee3928a7c69..f46c2085677b7 100644
--- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
+++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
@@ -29,10 +29,10 @@ interface DenormalizerInterface
/**
* Denormalizes data back into an object of the given class.
*
- * @param mixed $data Data to restore
- * @param string $type The expected class to instantiate
- * @param string|null $format Format the given data was extracted from
- * @param array $context Options available to the denormalizer
+ * @param mixed $data Data to restore
+ * @param string $type The expected class to instantiate
+ * @param string|null $format Format the given data was extracted from
+ * @param array $context Options available to the denormalizer
*
* @throws BadMethodCallException Occurs when the normalizer is not called in an expected context
* @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported
@@ -47,9 +47,10 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
/**
* Checks whether the given class is supported for denormalization by this normalizer.
*
- * @param mixed $data Data to denormalize from
- * @param string $type The class to which the data should be denormalized
- * @param string|null $format The format being deserialized from
+ * @param mixed $data Data to denormalize from
+ * @param string $type The class to which the data should be denormalized
+ * @param string|null $format The format being deserialized from
+ * @param array $context Options available to the denormalizer
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool;
diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
index bbc8a94e79da6..ea28b85818ae4 100644
--- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
+++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
@@ -24,9 +24,9 @@ interface NormalizerInterface
/**
* Normalizes data into a set of arrays/scalars.
*
- * @param mixed $data Data to normalize
- * @param string|null $format Format the normalization result will be encoded as
- * @param array $context Context options for the normalizer
+ * @param mixed $data Data to normalize
+ * @param string|null $format Format the normalization result will be encoded as
+ * @param array $context Context options for the normalizer
*
* @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array
*
@@ -41,8 +41,9 @@ public function normalize(mixed $data, ?string $format = null, array $context =
/**
* Checks whether the given class is supported for normalization by this normalizer.
*
- * @param mixed $data Data to normalize
- * @param string|null $format The format being (de-)serialized from or into
+ * @param mixed $data Data to normalize
+ * @param string|null $format The format being (de-)serialized from or into
+ * @param array $context Context options for the normalizer
*/
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool;
diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
index 1d60cba50b0c3..cbba35ba0f674 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
@@ -20,7 +20,6 @@
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Exception\LogicException;
-use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -149,30 +148,6 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v
}
}
- protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
- {
- if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
- return false;
- }
-
- if (null !== $this->classDiscriminatorResolver) {
- $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
- $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
- }
-
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
- $attributes = [];
- foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
- $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
- }
- $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
- }
- }
-
- return $allowedAttributes;
- }
-
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
index 9092433214abf..fa3a4117618ea 100644
--- a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
@@ -128,6 +128,40 @@ public function testAddDebugTraceIdInContext()
$traceableSerializer->encode('data', 'format');
$traceableSerializer->decode('data', 'format');
}
+
+ public function testCollectedCaller()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ $traceableSerializer->normalize('data');
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
+
+ public function testCollectedCallerFromArrayMap()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ array_map([$traceableSerializer, 'normalize'], ['data']);
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
}
class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
index 31206ea67d289..ea26589a2b072 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
@@ -20,6 +20,7 @@
'one' => DummyMessageNumberOne::class,
'two' => DummyMessageNumberTwo::class,
'three' => DummyMessageNumberThree::class,
+ 'four' => DummyMessageNumberFour::class,
])]
interface DummyMessageInterface
{
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php
new file mode 100644
index 0000000000000..eaf87d48a7101
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Fixtures;
+
+use Symfony\Component\Serializer\Attribute\Ignore;
+
+abstract class SomeAbstract {
+ #[Ignore]
+ public function getDescription()
+ {
+ return 'Hello, World!';
+ }
+}
+
+class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface
+{
+ public function __construct(public $one)
+ {
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
index 7068b8c8e6f49..50b9e2a83c26c 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
@@ -45,6 +45,7 @@
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
+use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
@@ -52,6 +53,8 @@
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberFour;
use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux;
use Symfony\Component\Serializer\Tests\Fixtures\DummyString;
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
@@ -1235,6 +1238,25 @@ public static function provideBooleanTypesData()
];
}
+ public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator()
+ {
+ $example = new DummyMessageNumberFour('Hello');
+
+ $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
+
+ $normalizer = new PropertyNormalizer(
+ $classMetadataFactory,
+ null,
+ new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]),
+ new ClassDiscriminatorFromClassMetadata($classMetadataFactory),
+ );
+
+ $serialized = $normalizer->normalize($example, 'json');
+ $deserialized = $normalizer->denormalize($serialized, DummyMessageInterface::class, 'json');
+
+ $this->assertEquals($example, $deserialized);
+ }
+
/**
* @dataProvider provideDenormalizeWithFilterBoolData
*/
diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json
index d8809fa079ef9..1a58ba7ee133b 100644
--- a/src/Symfony/Component/Serializer/composer.json
+++ b/src/Symfony/Component/Serializer/composer.json
@@ -24,26 +24,26 @@
"phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
"phpstan/phpdoc-parser": "^1.0|^2.0",
"seld/jsonlint": "^1.10",
- "symfony/cache": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/dependency-injection": "^7.2",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/filesystem": "^6.4|^7.0",
- "symfony/form": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/property-info": "^6.4|^7.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^7.2|^8.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/form": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3",
- "symfony/type-info": "^7.1",
- "symfony/uid": "^6.4|^7.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0",
- "symfony/yaml": "^6.4|^7.0"
+ "symfony/type-info": "^7.1|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2",
diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php
index cf280cdbac3c2..b6b8833b1482f 100644
--- a/src/Symfony/Component/String/AbstractUnicodeString.php
+++ b/src/Symfony/Component/String/AbstractUnicodeString.php
@@ -360,7 +360,7 @@ public function replaceMatches(string $fromRegexp, string|callable $to): static
public function reverse(): static
{
$str = clone $this;
- $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY)));
+ $str->string = implode('', array_reverse(grapheme_str_split($str->string)));
return $str;
}
diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
index 2433f895f5508..1c70993774c5e 100644
--- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
+++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
@@ -751,7 +751,7 @@ public static function provideReverse()
[
['äuß⭐erst', 'tsre⭐ßuä'],
['漢字ーユニコードéèΣσς', 'ςσΣèéドーコニユー字漢'],
- ['नमस्ते', 'तेस्मन'],
+ // ['नमस्ते', 'तेस्मन'], this case requires a version of intl that supports Unicode 15.1
]
);
}
diff --git a/src/Symfony/Component/String/composer.json b/src/Symfony/Component/String/composer.json
index 10d0ee620e4da..a7ae848dc392d 100644
--- a/src/Symfony/Component/String/composer.json
+++ b/src/Symfony/Component/String/composer.json
@@ -18,17 +18,17 @@
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-grapheme": "~1.33",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/emoji": "^7.1",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/emoji": "^7.1|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3.0",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/translation-contracts": "<2.5"
diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/composer.json b/src/Symfony/Component/Translation/Bridge/Crowdin/composer.json
index d2f60819d6b9b..700733a7d8c6a 100644
--- a/src/Symfony/Component/Translation/Bridge/Crowdin/composer.json
+++ b/src/Symfony/Component/Translation/Bridge/Crowdin/composer.json
@@ -21,9 +21,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/config": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/translation": "^7.2"
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\Crowdin\\": "" },
diff --git a/src/Symfony/Component/Translation/Bridge/Loco/composer.json b/src/Symfony/Component/Translation/Bridge/Loco/composer.json
index 40eb6f753d363..a98c6f91595b8 100644
--- a/src/Symfony/Component/Translation/Bridge/Loco/composer.json
+++ b/src/Symfony/Component/Translation/Bridge/Loco/composer.json
@@ -17,9 +17,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/translation": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\Loco\\": "" },
diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/composer.json b/src/Symfony/Component/Translation/Bridge/Lokalise/composer.json
index 78be5ea3c89cc..6f9a8c915e20b 100644
--- a/src/Symfony/Component/Translation/Bridge/Lokalise/composer.json
+++ b/src/Symfony/Component/Translation/Bridge/Lokalise/composer.json
@@ -17,9 +17,9 @@
],
"require": {
"php": ">=8.2",
- "symfony/config": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/translation": "^7.2"
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\Lokalise\\": "" },
diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/composer.json b/src/Symfony/Component/Translation/Bridge/Phrase/composer.json
index 2d3105037f7c6..3cd63db74b5a9 100644
--- a/src/Symfony/Component/Translation/Bridge/Phrase/composer.json
+++ b/src/Symfony/Component/Translation/Bridge/Phrase/composer.json
@@ -18,9 +18,9 @@
"require": {
"php": ">=8.2",
"psr/cache": "^3.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/translation": "^7.2"
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^7.2|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\Phrase\\": "" },
diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php
index 2d229f2dd1839..eac50bb1f9b9e 100644
--- a/src/Symfony/Component/Translation/MessageCatalogue.php
+++ b/src/Symfony/Component/Translation/MessageCatalogue.php
@@ -217,6 +217,16 @@ public function getMetadata(string $key = '', string $domain = 'messages'): mixe
return $this->metadata;
}
+ if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX])) {
+ if ('' === $key) {
+ return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX];
+ }
+
+ if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key])) {
+ return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key];
+ }
+ }
+
if (isset($this->metadata[$domain])) {
if ('' == $key) {
return $this->metadata[$domain];
diff --git a/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php
new file mode 100644
index 0000000000000..1ac61673999b2
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Tests\Catalogue;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\MessageCatalogue;
+
+class MessageCatalogueTest extends TestCase
+{
+ public function testIcuMetadataKept()
+ {
+ $mc = new MessageCatalogue('en', ['messages' => ['a' => 'new_a']]);
+ $metadata = ['metadata' => 'value'];
+ $mc->setMetadata('a', $metadata, 'messages+intl-icu');
+ $this->assertEquals($metadata, $mc->getMetadata('a', 'messages'));
+ $this->assertEquals($metadata, $mc->getMetadata('a', 'messages+intl-icu'));
+ }
+}
diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationLintCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationLintCommandTest.php
index 26d46d90d5415..5dad11d02d035 100644
--- a/src/Symfony/Component/Translation/Tests/Command/TranslationLintCommandTest.php
+++ b/src/Symfony/Component/Translation/Tests/Command/TranslationLintCommandTest.php
@@ -138,7 +138,11 @@ private function createCommand(Translator $translator, array $enabledLocales): C
$command = new TranslationLintCommand($translator, $enabledLocales);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return $command;
}
diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php
index c8ecf1cf9ae86..223703804a510 100644
--- a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php
+++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php
@@ -695,7 +695,12 @@ public function testPullMessagesMultipleDomains()
public function testComplete(array $input, array $expectedSuggestions)
{
$application = new Application();
- $application->add($this->createCommand($this->createMock(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], 'en', ['loco', 'crowdin', 'lokalise']));
+ $command = $this->createCommand($this->createMock(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], 'en', ['loco', 'crowdin', 'lokalise']);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('translation:pull'));
$suggestions = $tester->complete($input);
@@ -724,7 +729,11 @@ private function createCommandTester(ProviderInterface $provider, array $locales
{
$command = $this->createCommand($provider, $locales, $domains, $defaultLocale);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return new CommandTester($application->find('translation:pull'));
}
diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php
index 44cc569cfa276..5e113e1b116c0 100644
--- a/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php
+++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php
@@ -361,7 +361,11 @@ public function testPushWithProviderDomains()
);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandTester($application->find('translation:push'));
$tester->execute(['--locales' => ['en', 'fr']]);
@@ -375,7 +379,12 @@ public function testPushWithProviderDomains()
public function testComplete(array $input, array $expectedSuggestions)
{
$application = new Application();
- $application->add($this->createCommand($this->createMock(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], ['loco', 'crowdin', 'lokalise']));
+ $command = $this->createCommand($this->createMock(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], ['loco', 'crowdin', 'lokalise']);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('translation:push'));
$suggestions = $tester->complete($input);
@@ -404,7 +413,11 @@ private function createCommandTester(ProviderInterface $provider, array $locales
{
$command = $this->createCommand($provider, $locales, $domains);
$application = new Application();
- $application->add($command);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
return new CommandTester($application->find('translation:push'));
}
diff --git a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php
index 7b9fd1ae35b9d..b78ade960be7b 100644
--- a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php
+++ b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php
@@ -210,7 +210,12 @@ private function createCommand($requireStrictFileNames = true, $application = nu
{
if (!$application) {
$application = new Application();
- $application->add(new XliffLintCommand(null, null, null, $requireStrictFileNames));
+ $command = new XliffLintCommand(null, null, null, $requireStrictFileNames);
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
}
$command = $application->find('lint:xliff');
diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json
index ce9a7bf48c61b..69a2998af9390 100644
--- a/src/Symfony/Component/Translation/composer.json
+++ b/src/Symfony/Component/Translation/composer.json
@@ -23,17 +23,17 @@
},
"require-dev": {
"nikic/php-parser": "^5.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.21",
- "symfony/routing": "^6.4|^7.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
index 0b65137e4cdda..7f73190df1549 100644
--- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
@@ -12,11 +12,15 @@
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
/**
+ * @phpstan-type CustomArray = array{0: CustomInt, 1: CustomString, 2: bool}
* @phpstan-type CustomString = string
+ *
* @phpstan-import-type CustomInt from DummyWithPhpDoc
* @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt
*
+ * @psalm-type PsalmCustomArray = array{0: PsalmCustomInt, 1: PsalmCustomString, 2: bool}
* @psalm-type PsalmCustomString = string
+ *
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc as PsalmAliasedCustomInt
*/
@@ -53,9 +57,31 @@ final class DummyWithTypeAliases
public mixed $psalmOtherAliasedExternalAlias;
}
+/**
+ * @phpstan-type Foo = array{0: Bar}
+ * @phpstan-type Bar = array{0: Foo}
+ */
+final class DummyWithRecursiveTypeAliases
+{
+}
+
+/**
+ * @phpstan-type Invalid = SomethingInvalid
+ */
+final class DummyWithInvalidTypeAlias
+{
+}
+
/**
* @phpstan-import-type Invalid from DummyWithTypeAliases
*/
final class DummyWithInvalidTypeAliasImport
{
}
+
+/**
+ * @phpstan-import-type Invalid from int
+ */
+final class DummyWithTypeAliasImportedFromInvalidClassName
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
index e7794e4f114b6..7d6725ed26743 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
@@ -15,9 +15,12 @@
use Symfony\Component\TypeInfo\Exception\LogicException;
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAlias;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAliasImport;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithRecursiveTypeAliases;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliasImportedFromInvalidClassName;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
@@ -128,27 +131,33 @@ public function testCollectTypeAliases()
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
}
@@ -167,4 +176,28 @@ public function testThrowWhenImportingInvalidAlias()
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
}
+
+ public function testThrowWhenCannotResolveTypeAlias()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Cannot resolve "Invalid" type alias.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAlias::class);
+ }
+
+ public function testThrowWhenTypeAliasNotImportedFromValidClassName()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Type alias "Invalid" is not imported from a valid class name.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithTypeAliasImportedFromInvalidClassName::class);
+ }
+
+ public function testThrowWhenImportingRecursiveTypeAliases()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Cannot resolve "Bar" type alias.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithRecursiveTypeAliases::class)->typeAliases;
+ }
}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
index fcfe909cecf6e..9cdd9e1f196e9 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -79,6 +79,7 @@ public static function resolveDataProvider(): iterable
yield [Type::arrayShape(['foo' => Type::bool()], sealed: false), 'array{foo: bool, ...}'];
yield [Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::int(), extraValueType: Type::string()), 'array{foo: bool, ...}'];
yield [Type::arrayShape(['foo' => Type::bool()], extraValueType: Type::int()), 'array{foo: bool, ...}'];
+ yield [Type::arrayShape(['foo' => Type::union(Type::bool(), Type::float(), Type::int(), Type::null(), Type::string()), 'bar' => Type::string()]), 'array{foo: scalar|null, bar: string}'];
// object shape
yield [Type::object(), 'object{foo: true, bar: false}'];
@@ -157,6 +158,9 @@ public static function resolveDataProvider(): iterable
yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.''];
yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s>', \DateTime::class, \Stringable::class)];
yield [Type::int(), 'int<0, 100>'];
+ yield [Type::string(), \sprintf('value-of<%s>', DummyBackedEnum::class)];
+ yield [Type::int(), 'key-of>'];
+ yield [Type::bool(), 'value-of>'];
// union
yield [Type::union(Type::int(), Type::string()), 'int|string'];
@@ -216,9 +220,21 @@ public function testCannotResolveParentWithoutTypeContext()
$this->resolver->resolve('parent');
}
- public function testCannotUnknownIdentifier()
+ public function testCannotResolveUnknownIdentifier()
{
$this->expectException(UnsupportedException::class);
$this->resolver->resolve('unknown');
}
+
+ public function testCannotResolveKeyOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('key-of');
+ }
+
+ public function testCannotResolveValueOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('value-of');
+ }
}
diff --git a/src/Symfony/Component/TypeInfo/Type/CollectionType.php b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
index 80fbbdba6c3fa..a801f2b51f8d0 100644
--- a/src/Symfony/Component/TypeInfo/Type/CollectionType.php
+++ b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
@@ -65,25 +65,27 @@ public static function mergeCollectionValueTypes(array $types): Type
$boolTypes = [];
$objectTypes = [];
- foreach ($types as $t) {
- // cannot create an union with a standalone type
- if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) {
- return Type::mixed();
- }
+ foreach ($types as $type) {
+ foreach (($type instanceof UnionType ? $type->getTypes() : [$type]) as $t) {
+ // cannot create an union with a standalone type
+ if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) {
+ return Type::mixed();
+ }
- if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) {
- $boolTypes[] = $t;
+ if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) {
+ $boolTypes[] = $t;
- continue;
- }
+ continue;
+ }
- if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) {
- $objectTypes[] = $t;
+ if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) {
+ $objectTypes[] = $t;
- continue;
- }
+ continue;
+ }
- $normalizedTypes[] = $t;
+ $normalizedTypes[] = $t;
+ }
}
$boolTypes = array_unique($boolTypes);
diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
index d268c85fe49b0..a149a52249ba7 100644
--- a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
@@ -199,32 +199,85 @@ private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $t
}
$aliases = [];
- foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
- if (!$tag->value instanceof TypeAliasTagValueNode) {
+ $resolvedAliases = [];
+
+ foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
+ if (!$tag->value instanceof TypeAliasImportTagValueNode) {
continue;
}
- $aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext);
+ $importedFromType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
+ if (!$importedFromType instanceof ObjectType) {
+ throw new LogicException(\sprintf('Type alias "%s" is not imported from a valid class name.', $tag->value->importedAlias));
+ }
+
+ $importedFromContext = $this->createFromClassName($importedFromType->getClassName());
+
+ $typeAlias = $importedFromContext->typeAliases[$tag->value->importedAlias] ?? null;
+ if (!$typeAlias) {
+ throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedFromType->getClassName()));
+ }
+
+ $resolvedAliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
}
- foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
- if (!$tag->value instanceof TypeAliasImportTagValueNode) {
+ foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
+ if (!$tag->value instanceof TypeAliasTagValueNode) {
continue;
}
- /** @var ObjectType $importedType */
- $importedType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
- $importedTypeContext = $this->createFromClassName($importedType->getClassName());
+ $aliases[$tag->value->alias] = (string) $tag->value->type;
+ }
- $typeAlias = $importedTypeContext->typeAliases[$tag->value->importedAlias] ?? null;
- if (!$typeAlias) {
- throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedType->getClassName()));
+ return $this->resolveTypeAliases($aliases, $resolvedAliases, $typeContext);
+ }
+
+ /**
+ * @param array $toResolve
+ * @param array $resolved
+ *
+ * @return array
+ */
+ private function resolveTypeAliases(array $toResolve, array $resolved, TypeContext $typeContext): array
+ {
+ if (!$toResolve) {
+ return [];
+ }
+
+ $typeContext = new TypeContext(
+ $typeContext->calledClassName,
+ $typeContext->declaringClassName,
+ $typeContext->namespace,
+ $typeContext->uses,
+ $typeContext->templates,
+ $typeContext->typeAliases + $resolved,
+ );
+
+ $succeeded = false;
+ $lastFailure = null;
+ $lastFailingAlias = null;
+
+ foreach ($toResolve as $alias => $type) {
+ try {
+ $resolved[$alias] = $this->stringTypeResolver->resolve($type, $typeContext);
+ unset($toResolve[$alias]);
+ $succeeded = true;
+ } catch (UnsupportedException $lastFailure) {
+ $lastFailingAlias = $alias;
}
+ }
+
+ // nothing has succeeded, the result won't be different from the
+ // previous one, we can stop here.
+ if (!$succeeded) {
+ throw new LogicException(\sprintf('Cannot resolve "%s" type alias.', $lastFailingAlias), 0, $lastFailure);
+ }
- $aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
+ if ($toResolve) {
+ return $this->resolveTypeAliases($toResolve, $resolved, $typeContext);
}
- return $aliases;
+ return $resolved;
}
private function getPhpDocNode(string $rawDocNode): PhpDocNode
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
index 475e0212490d7..43457c9314fca 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -38,6 +38,7 @@
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\GenericType;
@@ -195,6 +196,28 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof GenericTypeNode) {
+ if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof BackedEnumType) {
+ return $type->getBackingType();
+ }
+
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionValueType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0]));
+ }
+
+ if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionKeyType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0]));
+ }
+
$type = $this->getTypeFromNode($node->type, $typeContext);
// handle integer ranges as simple integers
diff --git a/src/Symfony/Component/Uid/Tests/Command/GenerateUlidCommandTest.php b/src/Symfony/Component/Uid/Tests/Command/GenerateUlidCommandTest.php
index 7976b9e064fc1..f077e8e9e284a 100644
--- a/src/Symfony/Component/Uid/Tests/Command/GenerateUlidCommandTest.php
+++ b/src/Symfony/Component/Uid/Tests/Command/GenerateUlidCommandTest.php
@@ -109,7 +109,12 @@ public function testUlidsAreDifferentWhenGeneratingSeveralNow()
public function testComplete(array $input, array $expectedSuggestions)
{
$application = new Application();
- $application->add(new GenerateUlidCommand());
+ $command = new GenerateUlidCommand();
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('ulid:generate'));
$suggestions = $tester->complete($input, 2);
$this->assertSame($expectedSuggestions, $suggestions);
diff --git a/src/Symfony/Component/Uid/Tests/Command/GenerateUuidCommandTest.php b/src/Symfony/Component/Uid/Tests/Command/GenerateUuidCommandTest.php
index afea7873f8f0e..72d38febe643a 100644
--- a/src/Symfony/Component/Uid/Tests/Command/GenerateUuidCommandTest.php
+++ b/src/Symfony/Component/Uid/Tests/Command/GenerateUuidCommandTest.php
@@ -238,7 +238,12 @@ public function testNamespacePredefinedKeyword()
public function testComplete(array $input, array $expectedSuggestions)
{
$application = new Application();
- $application->add(new GenerateUuidCommand());
+ $command = new GenerateUuidCommand();
+ if (method_exists($application, 'addCommand')) {
+ $application->addCommand($command);
+ } else {
+ $application->add($command);
+ }
$tester = new CommandCompletionTester($application->get('uuid:generate'));
$suggestions = $tester->complete($input, 2);
$this->assertSame($expectedSuggestions, $suggestions);
diff --git a/src/Symfony/Component/Uid/composer.json b/src/Symfony/Component/Uid/composer.json
index 9843341c6d174..a3dd9f4401c94 100644
--- a/src/Symfony/Component/Uid/composer.json
+++ b/src/Symfony/Component/Uid/composer.json
@@ -24,7 +24,7 @@
"symfony/polyfill-uuid": "^1.15"
},
"require-dev": {
- "symfony/console": "^6.4|^7.0"
+ "symfony/console": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Uid\\": "" },
diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md
index a7363d7f59c19..e8146d2a50683 100644
--- a/src/Symfony/Component/Validator/CHANGELOG.md
+++ b/src/Symfony/Component/Validator/CHANGELOG.md
@@ -6,7 +6,55 @@ CHANGELOG
* Add the `filenameCharset` and `filenameCountUnit` options to the `File` constraint
* Deprecate defining custom constraints not supporting named arguments
+
+ Before:
+
+ ```php
+ use Symfony\Component\Validator\Constraint;
+
+ class CustomConstraint extends Constraint
+ {
+ public function __construct(array $options)
+ {
+ // ...
+ }
+ }
+ ```
+
+ After:
+
+ ```php
+ use Symfony\Component\Validator\Attribute\HasNamedArguments;
+ use Symfony\Component\Validator\Constraint;
+
+ class CustomConstraint extends Constraint
+ {
+ #[HasNamedArguments]
+ public function __construct($option1, $option2, $groups, $payload)
+ {
+ // ...
+ }
+ }
+ ```
* Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead
+
+ Before:
+
+ ```php
+ new NotNull([
+ 'groups' => ['foo', 'bar'],
+ 'message' => 'a custom constraint violation message',
+ ])
+ ```
+
+ After:
+
+ ```php
+ new NotNull(
+ groups: ['foo', 'bar'],
+ message: 'a custom constraint violation message',
+ )
+ ```
* Add support for ratio checks for SVG files to the `Image` constraint
* Add support for the `otherwise` option in the `When` constraint
* Add support for multiple fields containing nested constraints in `Composite` constraints
diff --git a/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php b/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
index b20ea0df0abe8..20d55f458b6b2 100644
--- a/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
+++ b/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
/**
@@ -39,6 +40,7 @@ class AtLeastOneOf extends Composite
* @param string|null $messageCollection Failure message for All and Collection inner constraints
* @param bool|null $includeInternalMessages Whether to include inner constraint messages (defaults to true)
*/
+ #[HasNamedArguments]
public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null, ?string $message = null, ?string $messageCollection = null, ?bool $includeInternalMessages = null)
{
if (\is_array($constraints) && !array_is_list($constraints)) {
diff --git a/src/Symfony/Component/Validator/Constraints/Cascade.php b/src/Symfony/Component/Validator/Constraints/Cascade.php
index a3ea71dbf90a9..7d90cfcf7f99f 100644
--- a/src/Symfony/Component/Validator/Constraints/Cascade.php
+++ b/src/Symfony/Component/Validator/Constraints/Cascade.php
@@ -36,6 +36,7 @@ public function __construct(array|string|null $exclude = null, ?array $options =
trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class);
$options = array_merge($exclude, $options ?? []);
+ $options['exclude'] = array_flip((array) ($options['exclude'] ?? []));
} else {
if (\is_array($options)) {
trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class);
diff --git a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
index a44694345aab0..7d5b20bf16ec6 100644
--- a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
@@ -47,7 +47,6 @@ public function validate(mixed $value, Constraint $constraint): void
$context = $this->context;
foreach ($constraint->fields as $field => $fieldConstraint) {
- // bug fix issue #2779
$existsInArray = \is_array($value) && \array_key_exists($field, $value);
$existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($field);
diff --git a/src/Symfony/Component/Validator/Constraints/Composite.php b/src/Symfony/Component/Validator/Constraints/Composite.php
index deac22cc5570d..ce6283b84f125 100644
--- a/src/Symfony/Component/Validator/Constraints/Composite.php
+++ b/src/Symfony/Component/Validator/Constraints/Composite.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@@ -49,6 +50,7 @@ abstract class Composite extends Constraint
* cached. When constraints are loaded from the cache, no more group
* checks need to be done.
*/
+ #[HasNamedArguments]
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
{
parent::__construct($options, $groups, $payload);
diff --git a/src/Symfony/Component/Validator/Constraints/Compound.php b/src/Symfony/Component/Validator/Constraints/Compound.php
index ac2b5ac9890ca..2618715335b79 100644
--- a/src/Symfony/Component/Validator/Constraints/Compound.php
+++ b/src/Symfony/Component/Validator/Constraints/Compound.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@@ -24,6 +25,7 @@ abstract class Compound extends Composite
/** @var Constraint[] */
public array $constraints = [];
+ #[HasNamedArguments]
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
{
if (isset($options[$this->getCompositeOption()])) {
diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php
index 3c2cc48ba815b..e3e4f47f9e0ae 100644
--- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php
+++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
+
/**
* A sequence of validation groups.
*
@@ -75,6 +77,7 @@ class GroupSequence
*
* @param array $groups The groups in the sequence
*/
+ #[HasNamedArguments]
public function __construct(array $groups)
{
$this->groups = $groups['value'] ?? $groups;
diff --git a/src/Symfony/Component/Validator/Constraints/Image.php b/src/Symfony/Component/Validator/Constraints/Image.php
index 5a4b3e12960e8..d9b7c8822e014 100644
--- a/src/Symfony/Component/Validator/Constraints/Image.php
+++ b/src/Symfony/Component/Validator/Constraints/Image.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
+
/**
* Validates that a file (or a path to a file) is a valid image.
*
@@ -118,6 +120,7 @@ class Image extends File
*
* @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types
*/
+ #[HasNamedArguments]
public function __construct(
?array $options = null,
int|string|null $maxSize = null,
diff --git a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
index 87d0b26794e71..7f1bfe2651550 100644
--- a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
@@ -44,7 +44,7 @@ public function validate(mixed $value, Constraint $constraint): void
$value = \Locale::canonicalize($value);
}
- if (!Locales::exists($value)) {
+ if (null === $value || !Locales::exists($value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($inputValue))
->setCode(Locale::NO_SUCH_LOCALE_ERROR)
diff --git a/src/Symfony/Component/Validator/Constraints/Sequentially.php b/src/Symfony/Component/Validator/Constraints/Sequentially.php
index 1096a994d0bb4..6389ebb890f3b 100644
--- a/src/Symfony/Component/Validator/Constraints/Sequentially.php
+++ b/src/Symfony/Component/Validator/Constraints/Sequentially.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
/**
@@ -28,6 +29,7 @@ class Sequentially extends Composite
* @param Constraint[]|array|null $constraints An array of validation constraints
* @param string[]|null $groups
*/
+ #[HasNamedArguments]
public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null)
{
if (\is_array($constraints) && !array_is_list($constraints)) {
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
index 2a2e559b95238..d5f48f0ae7ff2 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
@@ -468,7 +468,7 @@
This value is not a valid Twig template.
- Tato hodnota není platná šablona Twig.
+ Tato hodnota není platná Twig šablona.