diff --git a/CHANGELOG-6.2.md b/CHANGELOG-6.2.md index 94acdbdcfc990..0870e46a6ab8f 100644 --- a/CHANGELOG-6.2.md +++ b/CHANGELOG-6.2.md @@ -7,6 +7,18 @@ in 6.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.2.0...v6.2.1 +* 6.2.0-RC1 (2022-11-25) + + * bug #48312 [VarExporter] Improve partial-initialization API for ghost objects (nicolas-grekas) + * bug #48285 [Security] Support loading UserBadge directly from accessToken (Jeroeny) + * bug #48262 [Notifier] [SMSBiuras] `true`/`false` mismatch for `test_mode` option (StaffNowa) + * bug #48273 [HttpKernel] Fix message for unresovable arguments of invokable controllers (fancyweb) + * bug #48251 [PropertyInfo] ignore const expressions read by phpdocumentor (xabbuh) + * bug #48224 [DependencyInjection] Process bindings in `ServiceLocatorTagPass` (MatTheCat) + * bug #48271 [WebProfilerBundle] Fix form panel when there are no view vars (nicolas-grekas) + * bug #48274 Add more #[\SensitiveParameter] (fancyweb) + * bug #48179 [Console] Support completion for bash functions (Chi-teck) + * 6.2.0-BETA3 (2022-11-19) * bug #48217 [Console] Improve error message when shell is not detected in completion command (GromNaN) diff --git a/src/Symfony/Bridge/PhpUnit/ClockMock.php b/src/Symfony/Bridge/PhpUnit/ClockMock.php index 4f826f1c84b44..7ec22ccd85cb3 100644 --- a/src/Symfony/Bridge/PhpUnit/ClockMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClockMock.php @@ -97,7 +97,7 @@ public static function hrtime($asNumber = false) if ($asNumber) { $number = sprintf('%d%d', (int) self::$now, $ns); - return PHP_INT_SIZE === 8 ? (int) $number : (float) $number; + return \PHP_INT_SIZE === 8 ? (int) $number : (float) $number; } return [(int) self::$now, (int) $ns]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index e8ce7d8f95eec..b6bb058b3f170 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -30,7 +30,7 @@ class SodiumVault extends AbstractVault implements EnvVarLoaderInterface * @param $decryptionKey A string or a stringable object that defines the private key to use to decrypt the vault * or null to store generated keys in the provided $secretsDir */ - public function __construct(string $secretsDir, string|\Stringable $decryptionKey = null) + public function __construct(string $secretsDir, #[\SensitiveParameter] string|\Stringable $decryptionKey = null) { $this->pathPrefix = rtrim(strtr($secretsDir, '/', \DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.basename($secretsDir).'.'; $this->decryptionKey = $decryptionKey; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php index 1e38c58643638..b59f7974f1e87 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php @@ -79,9 +79,9 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token')) - ->replaceArgument(0, $userProvider) - ->replaceArgument(1, new Reference($config['token_handler'])) - ->replaceArgument(2, new Reference($extractorId)) + ->replaceArgument(0, new Reference($config['token_handler'])) + ->replaceArgument(1, new Reference($extractorId)) + ->replaceArgument(2, $userProvider) ->replaceArgument(3, $successHandler) ->replaceArgument(4, $failureHandler) ->replaceArgument(5, $config['realm']) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php index 17bc916c8d314..f1aea7cb2c3d1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php @@ -26,12 +26,12 @@ ->set('security.authenticator.access_token', AccessTokenAuthenticator::class) ->abstract() ->args([ - abstract_arg('user provider'), abstract_arg('access token handler'), abstract_arg('access token extractor'), null, null, null, + null, ]) ->call('setTranslator', [service('translator')->ignoreOnInvalid()]) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php index 0d1e9e0c0e7fc..4f94cc6936a05 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php @@ -13,6 +13,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface { @@ -20,10 +21,10 @@ public function __construct() { } - public function getUserIdentifierFrom(string $accessToken): string + public function getUserBadgeFrom(string $accessToken): UserBadge { return match ($accessToken) { - 'VALID_ACCESS_TOKEN' => 'dunglas', + 'VALID_ACCESS_TOKEN' => new UserBadge('dunglas'), default => throw new BadCredentialsException('Invalid credentials.'), }; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 2f2f21a9f45c1..1dad6d8a8361e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -714,7 +714,7 @@ - {% for variable, value in data.view_vars %} + {% for variable, value in data.view_vars ?? [] %} {{ variable }} {{ profiler_dump(value) }} diff --git a/src/Symfony/Component/Cache/Traits/Redis5Proxy.php b/src/Symfony/Component/Cache/Traits/Redis5Proxy.php index f9235b04df63f..bcfe8bc8d3288 100644 --- a/src/Symfony/Component/Cache/Traits/Redis5Proxy.php +++ b/src/Symfony/Component/Cache/Traits/Redis5Proxy.php @@ -29,9 +29,6 @@ class Redis5Proxy extends \Redis implements ResetInterface, LazyObjectInterface resetLazyObject as reset; } - private int $lazyObjectId; - private \Redis $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], diff --git a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php index 2304bd878684f..f5373df72f3bf 100644 --- a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php @@ -29,9 +29,6 @@ class Redis6Proxy extends \Redis implements ResetInterface, LazyObjectInterface resetLazyObject as reset; } - private int $lazyObjectId; - private \Redis $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster5Proxy.php b/src/Symfony/Component/Cache/Traits/RedisCluster5Proxy.php index 58e5ad5cbff9f..d95aced028137 100644 --- a/src/Symfony/Component/Cache/Traits/RedisCluster5Proxy.php +++ b/src/Symfony/Component/Cache/Traits/RedisCluster5Proxy.php @@ -29,9 +29,6 @@ class RedisCluster5Proxy extends \RedisCluster implements ResetInterface, LazyOb resetLazyObject as reset; } - private int $lazyObjectId; - private \RedisCluster $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php index bf61a4c2c25f5..9cc6abf7fad94 100644 --- a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php @@ -29,9 +29,6 @@ class RedisCluster6Proxy extends \RedisCluster implements ResetInterface, LazyOb resetLazyObject as reset; } - private int $lazyObjectId; - private \RedisCluster $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 76cf77df5e195..c345b4af7747f 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -430,7 +430,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $value = fgets($inputStream, 4096); - if (4095 === strlen($value)) { + if (4095 === \strlen($value)) { $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; $errOutput->warning('The value was possibly truncated by your shell or terminal emulator'); } diff --git a/src/Symfony/Component/Console/Resources/completion.bash b/src/Symfony/Component/Console/Resources/completion.bash index 4dd978e75b351..ad69eab0ff070 100644 --- a/src/Symfony/Component/Console/Resources/completion.bash +++ b/src/Symfony/Component/Console/Resources/completion.bash @@ -11,13 +11,14 @@ _sf_{{ COMMAND_NAME }}() { local sf_cmd="${COMP_WORDS[0]}" # for an alias, get the real script behind it - if [[ $(type -t $sf_cmd) == "alias" ]]; then + sf_cmd_type=$(type -t $sf_cmd) + if [[ $sf_cmd_type == "alias" ]]; then sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/") - else + elif [[ $sf_cmd_type == "file" ]]; then sf_cmd=$(type -p $sf_cmd) fi - if [ ! -x "$sf_cmd" ]; then + if [[ $sf_cmd_type != "function" && ! -x $sf_cmd ]]; then return 1 fi diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php index 7387a8a934f4d..67103a78522d3 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php @@ -40,6 +40,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed return self::register($this->container, $value->getValues()); } + if ($value instanceof Definition) { + $value->setBindings(parent::processValue($value->getBindings())); + } + if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) { return parent::processValue($value, $isRoot); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index 644876e81f20a..4d248306a0b7d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -214,6 +214,18 @@ public function testDefinitionOrderIsTheSame() static::assertSame(['service-2', 'service-1'], array_keys($factories)); } + + public function testBindingsAreProcessed() + { + $container = new ContainerBuilder(); + + $definition = $container->register('foo') + ->setBindings(['foo' => new ServiceLocatorArgument()]); + + (new ServiceLocatorTagPass())->process($container); + + $this->assertInstanceOf(Reference::class, $definition->getBindings()['foo']->getValues()[0]); + } } class Locator diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php index 32e5364ef4e35..a7972921fadba 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy_ghost.php @@ -75,8 +75,6 @@ class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExport { use \Symfony\Component\VarExporter\LazyGhostTrait; - private int $lazyObjectId; - private const LAZY_OBJECT_PROPERTY_SCOPES = []; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt index d04e886a810b2..a52a32a27dadd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt @@ -45,8 +45,6 @@ class FooLazyClass_f814e3a extends \Bar\FooLazyClass implements \Symfony\Compone { use \Symfony\Component\VarExporter\LazyGhostTrait; - private int $lazyObjectId; - private const LAZY_OBJECT_PROPERTY_SCOPES = []; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php index f0143cb590f3f..c304c9461fd1c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php @@ -79,8 +79,6 @@ class stdClass_5a8a5eb extends \stdClass implements \Symfony\Component\VarExport { use \Symfony\Component\VarExporter\LazyGhostTrait; - private int $lazyObjectId; - private const LAZY_OBJECT_PROPERTY_SCOPES = []; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index ec39e663e4ec7..5de8772d6746e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -75,9 +75,6 @@ class Wither_94fa281 extends \Symfony\Component\DependencyInjection\Tests\Compil { use \Symfony\Component\VarExporter\LazyProxyTrait; - private int $lazyObjectId; - private parent $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 00a757d637645..c7ab562f2ef75 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -1768,7 +1768,7 @@ public function testDumpToProtectedDirectory() $this->markTestSkipped('This test is specific to Windows.'); } - if (($userProfilePath = getenv('USERPROFILE')) === false || !is_dir($userProfilePath)) { + if (false === ($userProfilePath = getenv('USERPROFILE')) || !is_dir($userProfilePath)) { throw new \RuntimeException('Failed to retrieve user profile path.'); } diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 790767d728190..fdea3d67e1b19 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -104,7 +104,7 @@ CHANGELOG make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to update your database. * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, - make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database + make sure to run `CREATE INDEX expiry ON sessions (sess_lifetime)` to update your database to speed up garbage collection of expired sessions. * added `SessionHandlerFactory` to create session handlers with a DSN * added `IpUtils::anonymize()` to help with GDPR compliance. diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 9e26ac56e1249..302372627ddb5 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -207,7 +207,7 @@ public function createTable() try { $this->pdo->exec($sql); - $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)"); + $this->pdo->exec("CREATE INDEX expiry ON $this->table ($this->lifetimeCol)"); } catch (\PDOException $e) { $this->rollback(); diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php index 006ab29d46e04..26403612880fe 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php @@ -72,8 +72,10 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $controller = ltrim($controller, '\\'); } - if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) { - $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); + if (!$this->container->has($controller)) { + $controller = (false !== $i = strrpos($controller, ':')) + ? substr($controller, 0, $i).strtolower(substr($controller, $i)) + : $controller.'::__invoke'; } if ($this->container->has($controller)) { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php index eb4ac7b06a217..edc30e1806c13 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php @@ -42,8 +42,6 @@ public function supports(Request $request, ArgumentMetadata $argument): bool return true; } - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - $method = \get_class($this->inner).'::'.__FUNCTION__; $this->stopwatch->start($method, 'controller.argument_value_resolver'); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 613356dd5af51..52a61b868d73c 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -75,12 +75,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.2.0-BETA3'; + public const VERSION = '6.2.0-RC1'; public const VERSION_ID = 60200; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 2; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'BETA3'; + public const EXTRA_VERSION = 'RC1'; public const END_OF_MAINTENANCE = '07/2023'; public const END_OF_LIFE = '07/2023'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php index 25214afdfa48a..71b5543b04092 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php @@ -108,6 +108,22 @@ public function testControllerNameIsAnArray() $resolver->resolve($request, $argument); } + /** + * In Symfony 7, keep this test case but remove the call to supports(). + * + * @group legacy + */ + public function testInvokableController() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::__invoke()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); + $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); + $request = $this->requestWithAttributes(['_controller' => 'App\Controller\Mine']); + $this->assertTrue($resolver->supports($request, $argument)); + $resolver->resolve($request, $argument); + } + private function requestWithAttributes(array $attributes) { $request = Request::create('/'); diff --git a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/SmsBiurasTransport.php b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/SmsBiurasTransport.php index 10814f8fd18ca..bc6257422fe50 100644 --- a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/SmsBiurasTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/SmsBiurasTransport.php @@ -87,7 +87,7 @@ protected function doSend(MessageInterface $message): SentMessage 'apikey' => $this->apiKey, 'message' => $message->getSubject(), 'from' => $from, - 'test' => $this->testMode ? 0 : 1, + 'test' => $this->testMode ? 1 : 0, 'to' => $message->getPhone(), ], ]); diff --git a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/Tests/SmsBiurasTransportTest.php b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/Tests/SmsBiurasTransportTest.php index 28e96cc491877..76cb97a39cc76 100644 --- a/src/Symfony/Component/Notifier/Bridge/SmsBiuras/Tests/SmsBiurasTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/SmsBiuras/Tests/SmsBiurasTransportTest.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Notifier\Bridge\SmsBiuras\Tests; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\Notifier\Bridge\SmsBiuras\SmsBiurasTransport; use Symfony\Component\Notifier\Message\ChatMessage; use Symfony\Component\Notifier\Message\MessageInterface; use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Notifier\Test\TransportTestCase; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; final class SmsBiurasTransportTest extends TransportTestCase { @@ -40,4 +42,48 @@ public function unsupportedMessagesProvider(): iterable yield [new ChatMessage('Hello!')]; yield [$this->createMock(MessageInterface::class)]; } + + /** + * @dataProvider provideTestMode() + */ + public function testTestMode(int $expected, bool $testMode) + { + $message = new SmsMessage('+37012345678', 'Hello World!'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->atLeast(1)) + ->method('getStatusCode') + ->willReturn(200); + $response->expects($this->atLeast(1)) + ->method('getContent') + ->willReturn('OK: 519545'); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $message, $expected): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame(sprintf( + 'https://savitarna.smsbiuras.lt/api?uid=uid&apikey=api_key&message=%s&from=from&test=%s&to=%s', + rawurlencode($message->getSubject()), + $expected, + rawurlencode($message->getPhone()) + ), $url); + $this->assertSame($expected, $options['query']['test']); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('OK: 519545', $response->getContent()); + + return $response; + }); + + $transport = new SmsBiurasTransport('uid', 'api_key', 'from', $testMode, $client); + + $sentMessage = $transport->send($message); + + $this->assertSame('519545', $sentMessage->getMessageId()); + } + + public static function provideTestMode(): iterable + { + yield [1, true]; + yield [0, false]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index a9c7c1b478f45..6cf083bb4ee86 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo\Util; use phpDocumentor\Reflection\PseudoType; +use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\List_; use phpDocumentor\Reflection\Type as DocType; use phpDocumentor\Reflection\Types\Array_; @@ -42,6 +43,11 @@ final class PhpDocTypeHelper */ public function getTypes(DocType $varType): array { + if ($varType instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return []; + } + $types = []; $nullable = false; @@ -67,6 +73,11 @@ public function getTypes(DocType $varType): array for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) { $type = $varType->get($typeIndex); + if ($type instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return []; + } + // If null is present, all types are nullable if ($type instanceof Null_) { $nullable = true; diff --git a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php index 7d6dbda5489f4..7cefe8b0a0572 100644 --- a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php +++ b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php @@ -79,7 +79,7 @@ public function getToken(string $tokenId): CsrfToken return new CsrfToken($tokenId, $this->randomize($value)); } - public function refreshToken(#[\SensitiveParameter] string $tokenId): CsrfToken + public function refreshToken(string $tokenId): CsrfToken { $namespacedId = $this->getNamespace().$tokenId; $value = $this->generator->generateToken(); diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php index 15bdaaf82046e..cf0ce03e9311d 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php @@ -51,7 +51,7 @@ public function getToken(string $tokenId): string return (string) $_SESSION[$this->namespace][$tokenId]; } - public function setToken(string $tokenId, string $token) + public function setToken(string $tokenId, #[\SensitiveParameter] string $token) { if (!$this->sessionStarted) { $this->startSession(); diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php index bed96fa4c2960..fdbaf135d7654 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php @@ -56,7 +56,7 @@ public function getToken(string $tokenId): string return (string) $session->get($this->namespace.'/'.$tokenId); } - public function setToken(string $tokenId, string $token) + public function setToken(string $tokenId, #[\SensitiveParameter] string $token) { $session = $this->getSession(); if (!$session->isStarted()) { diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php b/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php index a26439366e2ab..d119d6e977bcb 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php @@ -28,7 +28,7 @@ public function getToken(string $tokenId): string; /** * Stores a CSRF token. */ - public function setToken(string $tokenId, string $token); + public function setToken(string $tokenId, #[\SensitiveParameter] string $token); /** * Removes a CSRF token. diff --git a/src/Symfony/Component/Security/Http/AccessToken/AccessTokenHandlerInterface.php b/src/Symfony/Component/Security/Http/AccessToken/AccessTokenHandlerInterface.php index 33a0690d15cc5..5cbc857a1c6f0 100644 --- a/src/Symfony/Component/Security/Http/AccessToken/AccessTokenHandlerInterface.php +++ b/src/Symfony/Component/Security/Http/AccessToken/AccessTokenHandlerInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\AccessToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; /** * The token handler retrieves the user identifier from the token. @@ -24,5 +25,5 @@ interface AccessTokenHandlerInterface /** * @throws AuthenticationException */ - public function getUserIdentifierFrom(string $accessToken): string; + public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php index ae8f0a4ea3da2..d3ade9b0be73b 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php @@ -21,7 +21,6 @@ use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; -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\Authenticator\Token\PostAuthenticationToken; @@ -38,9 +37,9 @@ class AccessTokenAuthenticator implements AuthenticatorInterface private ?TranslatorInterface $translator = null; public function __construct( - private readonly UserProviderInterface $userProvider, private readonly AccessTokenHandlerInterface $accessTokenHandler, private readonly AccessTokenExtractorInterface $accessTokenExtractor, + private readonly ?UserProviderInterface $userProvider = null, private readonly ?AuthenticationSuccessHandlerInterface $successHandler = null, private readonly ?AuthenticationFailureHandlerInterface $failureHandler = null, private readonly ?string $realm = null, @@ -58,11 +57,13 @@ public function authenticate(Request $request): Passport if (!$accessToken) { throw new BadCredentialsException('Invalid credentials.'); } - $userIdentifier = $this->accessTokenHandler->getUserIdentifierFrom($accessToken); - return new SelfValidatingPassport( - new UserBadge($userIdentifier, $this->userProvider->loadUserByIdentifier(...)) - ); + $userBadge = $this->accessTokenHandler->getUserBadgeFrom($accessToken); + if (null === $userBadge->getUserLoader() && $this->userProvider) { + $userBadge->setUserLoader($this->userProvider->loadUserByIdentifier(...)); + } + + return new SelfValidatingPassport($userBadge); } public function createToken(Passport $passport, string $firewallName): TokenInterface diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php index b24e2c75f548c..52aeafe858eb1 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php @@ -33,7 +33,7 @@ class CsrfTokenBadge implements BadgeInterface * Using a different string for each authenticator improves its security. * @param string|null $csrfToken The CSRF token presented in the request, if any */ - public function __construct(string $csrfTokenId, ?string $csrfToken) + public function __construct(string $csrfTokenId, #[\SensitiveParameter] ?string $csrfToken) { $this->csrfTokenId = $csrfTokenId; $this->csrfToken = $csrfToken; diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php index 992b50d0811c1..9cbf6d6e69343 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php @@ -32,7 +32,7 @@ class PasswordUpgradeBadge implements BadgeInterface * @param string $plaintextPassword The presented password, used in the rehash * @param PasswordUpgraderInterface|null $passwordUpgrader The password upgrader, defaults to the UserProvider if null */ - public function __construct(string $plaintextPassword, PasswordUpgraderInterface $passwordUpgrader = null) + public function __construct(#[\SensitiveParameter] string $plaintextPassword, PasswordUpgraderInterface $passwordUpgrader = null) { $this->plaintextPassword = $plaintextPassword; $this->passwordUpgrader = $passwordUpgrader; diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php index e59ce918b0651..8dd3188eb9587 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor; use Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor; use Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Tests\Authenticator\InMemoryAccessTokenHandler; @@ -40,7 +41,7 @@ protected function setUp(): void /** * @dataProvider provideSupportData */ - public function testSupport($request): void + public function testSupport($request) { $this->setUpAuthenticator(); @@ -53,9 +54,9 @@ public function provideSupportData(): iterable yield [new Request([], [], [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer INVALID_ACCESS_TOKEN'])]; } - public function testAuthenticate(): void + public function testAuthenticate() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator(); $request = new Request([], [], [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer VALID_ACCESS_TOKEN']); @@ -66,7 +67,7 @@ public function testAuthenticate(): void /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class): void + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); @@ -100,13 +101,13 @@ public function provideInvalidAuthenticateData(): iterable private function setUpAuthenticator(): void { $this->authenticator = new AccessTokenAuthenticator( - $this->userProvider, $this->accessTokenHandler, new ChainAccessTokenExtractor([ new FormEncodedBodyExtractor(), new QueryAccessTokenExtractor(), new HeaderAccessTokenExtractor(), - ]) + ]), + $this->userProvider ); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php index 5f251bb71f197..b915a5d10631c 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor; use Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Tests\Authenticator\InMemoryAccessTokenHandler; @@ -34,7 +35,7 @@ protected function setUp(): void $this->accessTokenHandler = new InMemoryAccessTokenHandler(); } - public function testSupport(): void + public function testSupport() { $this->setUpAuthenticator(); $request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded']); @@ -44,7 +45,7 @@ public function testSupport(): void $this->assertNull($this->authenticator->supports($request)); } - public function testSupportsWithCustomParameter(): void + public function testSupportsWithCustomParameter() { $this->setUpAuthenticator('protection-token'); $request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded']); @@ -54,9 +55,9 @@ public function testSupportsWithCustomParameter(): void $this->assertNull($this->authenticator->supports($request)); } - public function testAuthenticate(): void + public function testAuthenticate() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator(); $request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded'], 'access_token=VALID_ACCESS_TOKEN'); $request->request->set('access_token', 'VALID_ACCESS_TOKEN'); @@ -66,9 +67,9 @@ public function testAuthenticate(): void $this->assertInstanceOf(SelfValidatingPassport::class, $passport); } - public function testAuthenticateWithCustomParameter(): void + public function testAuthenticateWithCustomParameter() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator('protection-token'); $request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded']); $request->request->set('protection-token', 'VALID_ACCESS_TOKEN'); @@ -81,7 +82,7 @@ public function testAuthenticateWithCustomParameter(): void /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class): void + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); @@ -119,9 +120,9 @@ public function provideInvalidAuthenticateData(): iterable private function setUpAuthenticator(string $parameter = 'access_token'): void { $this->authenticator = new AccessTokenAuthenticator( - $this->userProvider, $this->accessTokenHandler, - new FormEncodedBodyExtractor($parameter) + new FormEncodedBodyExtractor($parameter), + $this->userProvider ); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php index 89e91e34feecf..a4e7758bffed5 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor; use Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Tests\Authenticator\InMemoryAccessTokenHandler; @@ -37,7 +38,7 @@ protected function setUp(): void /** * @dataProvider provideSupportData */ - public function testSupport($request): void + public function testSupport($request) { $this->setUpAuthenticator(); @@ -53,7 +54,7 @@ public function provideSupportData(): iterable /** * @dataProvider provideSupportsWithCustomTokenTypeData */ - public function testSupportsWithCustomTokenType($request, $result): void + public function testSupportsWithCustomTokenType($request, $result) { $this->setUpAuthenticator('Authorization', 'JWT'); @@ -71,7 +72,7 @@ public function provideSupportsWithCustomTokenTypeData(): iterable /** * @dataProvider provideSupportsWithCustomHeaderParameter */ - public function testSupportsWithCustomHeaderParameter($request, $result): void + public function testSupportsWithCustomHeaderParameter($request, $result) { $this->setUpAuthenticator('X-FOO'); @@ -86,9 +87,9 @@ public function provideSupportsWithCustomHeaderParameter(): iterable yield [new Request([], [], [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer INVALID_ACCESS_TOKEN']), false]; } - public function testAuthenticate(): void + public function testAuthenticate() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator(); $request = new Request([], [], [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer VALID_ACCESS_TOKEN']); @@ -96,9 +97,9 @@ public function testAuthenticate(): void $this->assertInstanceOf(SelfValidatingPassport::class, $passport); } - public function testAuthenticateWithCustomTokenType(): void + public function testAuthenticateWithCustomTokenType() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator('Authorization', 'JWT'); $request = new Request([], [], [], [], [], ['HTTP_AUTHORIZATION' => 'JWT VALID_ACCESS_TOKEN']); @@ -109,7 +110,7 @@ public function testAuthenticateWithCustomTokenType(): void /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class): void + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); @@ -143,9 +144,9 @@ public function provideInvalidAuthenticateData(): iterable private function setUpAuthenticator(string $headerParameter = 'Authorization', string $tokenType = 'Bearer'): void { $this->authenticator = new AccessTokenAuthenticator( - $this->userProvider, $this->accessTokenHandler, - new HeaderAccessTokenExtractor($headerParameter, $tokenType) + new HeaderAccessTokenExtractor($headerParameter, $tokenType), + $this->userProvider ); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php index c1a8206115452..73f8392a9fa94 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor; use Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Tests\Authenticator\InMemoryAccessTokenHandler; @@ -34,7 +35,7 @@ protected function setUp(): void $this->accessTokenHandler = new InMemoryAccessTokenHandler(); } - public function testSupport(): void + public function testSupport() { $this->setUpAuthenticator(); $request = new Request(); @@ -43,7 +44,7 @@ public function testSupport(): void $this->assertNull($this->authenticator->supports($request)); } - public function testSupportsWithCustomParameter(): void + public function testSupportsWithCustomParameter() { $this->setUpAuthenticator('protection-token'); $request = new Request(); @@ -52,9 +53,9 @@ public function testSupportsWithCustomParameter(): void $this->assertNull($this->authenticator->supports($request)); } - public function testAuthenticate(): void + public function testAuthenticate() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator(); $request = new Request(); $request->query->set('access_token', 'VALID_ACCESS_TOKEN'); @@ -63,9 +64,9 @@ public function testAuthenticate(): void $this->assertInstanceOf(SelfValidatingPassport::class, $passport); } - public function testAuthenticateWithCustomParameter(): void + public function testAuthenticateWithCustomParameter() { - $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', 'foo'); + $this->accessTokenHandler->add('VALID_ACCESS_TOKEN', new UserBadge('foo')); $this->setUpAuthenticator('protection-token'); $request = new Request(); $request->query->set('protection-token', 'VALID_ACCESS_TOKEN'); @@ -77,7 +78,7 @@ public function testAuthenticateWithCustomParameter(): void /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class): void + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); @@ -111,9 +112,9 @@ public function provideInvalidAuthenticateData(): iterable private function setUpAuthenticator(string $parameter = 'access_token'): void { $this->authenticator = new AccessTokenAuthenticator( - $this->userProvider, $this->accessTokenHandler, - new QueryAccessTokenExtractor($parameter) + new QueryAccessTokenExtractor($parameter), + $this->userProvider ); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/InMemoryAccessTokenHandler.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/InMemoryAccessTokenHandler.php index 9ace3ba8324a7..3fe4c850736ce 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/InMemoryAccessTokenHandler.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/InMemoryAccessTokenHandler.php @@ -13,15 +13,16 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class InMemoryAccessTokenHandler implements AccessTokenHandlerInterface { /** - * @var array + * @var array */ private $accessTokens = []; - public function getUserIdentifierFrom(string $accessToken): string + public function getUserBadgeFrom(string $accessToken): UserBadge { if (!\array_key_exists($accessToken, $this->accessTokens)) { throw new BadCredentialsException('Invalid access token or invalid user.'); @@ -37,7 +38,7 @@ public function remove(string $accessToken): self return $this; } - public function add(string $accessToken, string $user): self + public function add(string $accessToken, UserBadge $user): self { $this->accessTokens[$accessToken] = $user; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php index bfd4336de6e33..f44e606a49f12 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntax; use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntaxValidator; +use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; /** @@ -21,7 +22,7 @@ */ class ExpressionLanguageSyntaxValidatorTest extends ConstraintValidatorTestCase { - protected function createValidator(): ExpressionLanguageSyntaxValidator + protected function createValidator(): ConstraintValidatorInterface { return new ExpressionLanguageSyntaxValidator(new ExpressionLanguage()); } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 605f1fdd52831..99f721df2605c 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -55,16 +55,34 @@ public function initialize($instance, $propertyName, $propertyScope) $propertyScopes = Hydrator::$propertyScopes[$class]; $propertyScopes[$k = "\0$propertyScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName; - if (!$initializer = $this->initializer[$k] ?? null) { - return self::STATUS_UNINITIALIZED_PARTIAL; - } + if ($initializer = $this->initializer[$k] ?? null) { + $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); + $accessor['set']($instance, $propertyName, $value); - $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + return $this->status = self::STATUS_INITIALIZED_PARTIAL; + } - $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); - $accessor['set']($instance, $propertyName, $value); + $status = self::STATUS_UNINITIALIZED_PARTIAL; + + if ($initializer = $this->initializer["\0"] ?? null) { + if (!\is_array($values = $initializer($instance, LazyObjectRegistry::$defaultProperties[$class]))) { + throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values))); + } + $properties = (array) $instance; + foreach ($values as $key => $value) { + if ($k === $key) { + $status = self::STATUS_INITIALIZED_PARTIAL; + } + if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { + $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); + $accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope); + $accessor['set']($instance, $name, $value); + } + } + } - return $this->status = self::STATUS_INITIALIZED_PARTIAL; + return $status; } $this->status = self::STATUS_INITIALIZED_FULL; diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 16e40f7234567..df40c267fd027 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -15,11 +15,10 @@ use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry; use Symfony\Component\VarExporter\Internal\LazyObjectState; -/** - * @property int $lazyObjectId This property must be declared as private in classes using this trait - */ trait LazyGhostTrait { + private int $lazyObjectId; + /** * Creates a lazy-loading ghost instance. * @@ -30,13 +29,23 @@ trait LazyGhostTrait * properties and closures should accept 4 arguments: the instance to * initialize, the property to initialize, its write-scope, and its default * value. Each closure should return the value of the corresponding property. + * The special "\0" key can be used to define a closure that returns all + * properties at once when full-initialization is needed; it takes the + * instance and its default properties as arguments. + * + * Properties should be indexed by their array-cast name, see + * https://php.net/manual/language.types.array#language.types.array.casting * - * @param \Closure(static):void|array $initializer - * @param array $skippedProperties An array indexed by the properties to skip, aka the ones - * that the initializer doesn't set when its a closure + * @param (\Closure(static):void + * |array + * |array{"\0": \Closure(static, array):array}) $initializer + * @param array|null $skippedProperties An array indexed by the properties to skip, aka the ones + * that the initializer doesn't set when its a closure */ - public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = [], self $instance = null): static + public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = null, self $instance = null): static { + $onlyProperties = null === $skippedProperties && \is_array($initializer) ? $initializer : null; + if (self::class !== $class = $instance ? $instance::class : static::class) { $skippedProperties["\0".self::class."\0lazyObjectId"] = true; } elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) { @@ -46,8 +55,7 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp $instance ??= (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor(); Registry::$defaultProperties[$class] ??= (array) $instance; $instance->lazyObjectId = $id = spl_object_id($instance); - Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties); - $onlyProperties = \is_array($initializer) ? $initializer : null; + Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties ??= []); foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { $reset($instance, $skippedProperties, $onlyProperties); @@ -58,8 +66,10 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool + public function isLazyObjectInitialized(bool $partial = false): bool { if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { return true; @@ -71,6 +81,11 @@ public function isLazyObjectInitialized(): bool $class = $this::class; $properties = (array) $this; + + if ($partial) { + return (bool) array_intersect_key($state->initializer, $properties); + } + $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); foreach ($state->initializer as $key => $initializer) { if (!\array_key_exists($key, $properties) && isset($propertyScopes[$key])) { @@ -98,6 +113,8 @@ public function initializeLazyObject(): static return $this; } + $values = isset($state->initializer["\0"]) ? null : []; + $class = $this::class; $properties = (array) $this; $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); @@ -105,9 +122,25 @@ public function initializeLazyObject(): static if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { continue; } + $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); - $state->initialize($this, $name, $readonlyScope ?? ('*' !== $scope ? $scope : null)); - $properties = (array) $this; + if (null === $values) { + if (!\is_array($values = ($state->initializer["\0"])($this, Registry::$defaultProperties[$class]))) { + throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values))); + } + + if (\array_key_exists($key, $properties = (array) $this)) { + continue; + } + } + + if (\array_key_exists($key, $values)) { + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + $accessor['set']($this, $name, $properties[$key] = $values[$key]); + } else { + $state->initialize($this, $name, $scope); + $properties = (array) $this; + } } return $this; diff --git a/src/Symfony/Component/VarExporter/LazyObjectInterface.php b/src/Symfony/Component/VarExporter/LazyObjectInterface.php index 314ba85e37039..36708845912ca 100644 --- a/src/Symfony/Component/VarExporter/LazyObjectInterface.php +++ b/src/Symfony/Component/VarExporter/LazyObjectInterface.php @@ -15,8 +15,10 @@ interface LazyObjectInterface { /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool; + public function isLazyObjectInitialized(bool $partial = false): bool; /** * Forces initialization of a lazy object and returns it. diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 4b58b7a388160..97509eb35321b 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -16,13 +16,11 @@ use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry; use Symfony\Component\VarExporter\Internal\LazyObjectState; -/** - * @property int $lazyObjectId This property must be declared as private in classes using this trait - * @property parent $lazyObjectReal This property must be declared as private in classes using this trait; - * its type should match the type of the proxied object - */ trait LazyProxyTrait { + private int $lazyObjectId; + private object $lazyObjectReal; + /** * Creates a lazy-loading virtual proxy. * @@ -49,8 +47,10 @@ public static function createLazyProxy(\Closure $initializer, self $instance = n /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool + public function isLazyObjectInitialized(bool $partial = false): bool { if (0 >= ($this->lazyObjectId ?? 0)) { return true; diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 3df1dd14a3a4a..ba24c4afb08a0 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -27,6 +27,9 @@ final class ProxyHelper */ public static function generateLazyGhost(\ReflectionClass $class): string { + if (\PHP_VERSION_ID >= 80200 && $class->isReadOnly()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is read-only.', $class->name)); + } if ($class->isFinal()) { throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name)); } @@ -56,15 +59,12 @@ public static function generateLazyGhost(\ReflectionClass $class): string } } $propertyScopes = self::exportPropertyScopes($class->name); - $readonly = \PHP_VERSION_ID >= 80200 && $class->isReadOnly() ? 'readonly ' : ''; return <<name} implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyGhostTrait; - private {$readonly}int \$lazyObjectId; - private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes}; } @@ -91,6 +91,9 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf if ($class?->isFinal()) { throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name)); } + if (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly()) { + throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is read-only.', $class->name)); + } $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []]; foreach ($interfaces as $interface) { @@ -176,7 +179,6 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $methods[$lcName] = " {$signature}\n {\n{$body}\n }"; } - $readonly = \PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : ''; $types = $interfaces = array_unique(array_column($interfaces, 'name')); $interfaces[] = LazyObjectInterface::class; $interfaces = implode(', \\', $interfaces); @@ -198,9 +200,6 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf { use \Symfony\Component\VarExporter\LazyProxyTrait; - private {$readonly}int \$lazyObjectId; - private {$readonly}{$type} \$lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\\0".self::class."\\0lazyObjectReal" => [self::class, 'lazyObjectReal', null],{$propertyScopes} diff --git a/src/Symfony/Component/VarExporter/README.md b/src/Symfony/Component/VarExporter/README.md index 0107f99cae249..7c7a58e298357 100644 --- a/src/Symfony/Component/VarExporter/README.md +++ b/src/Symfony/Component/VarExporter/README.md @@ -92,8 +92,6 @@ when accessing a property. class FooLazyGhost extends Foo { use LazyGhostTrait; - - private int $lazyObjectId; } $foo = FooLazyGhost::createLazyGhost(initializer: function (Foo $instance): void { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php index a7dc5d3d73fb4..6cac9ffc03d01 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php @@ -18,6 +18,5 @@ class ChildMagicClass extends MagicClass implements LazyObjectInterface { use LazyGhostTrait; - private int $lazyObjectId; private int $data = 123; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php index 0a131d9bfb5c1..265fb1d318c38 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildStdClass.php @@ -17,6 +17,4 @@ class ChildStdClass extends \stdClass implements LazyObjectInterface { use LazyGhostTrait; - - private int $lazyObjectId; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/LazyClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/LazyClass.php index 710b0571b1755..a3ce88d14e942 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/LazyClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/LazyClass.php @@ -19,7 +19,6 @@ class LazyClass createLazyGhost as private; } - private int $lazyObjectId; public int $public; public function __construct(\Closure $initializer) diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php index 1c1127d546399..f755235831b1e 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/TestClass.php @@ -17,7 +17,6 @@ class TestClass extends NoMagicClass { use LazyGhostTrait; - private int $lazyObjectId; public int $public = 1; protected int $protected = 2; protected readonly int $protectedReadonly; diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index a64747490bd0c..3663217435268 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -248,7 +248,8 @@ public function testPartialInitialization() $this->assertFalse($instance->isLazyObjectInitialized()); $this->assertSame(123, $instance->public); $this->assertFalse($instance->isLazyObjectInitialized()); - $this->assertSame(["\0".TestClass::class."\0lazyObjectId", 'public'], array_keys((array) $instance)); + $this->assertTrue($instance->isLazyObjectInitialized(true)); + $this->assertSame(['public', "\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertSame(1, $counter); $instance->initializeLazyObject(); @@ -330,4 +331,89 @@ public function testReflectionPropertyGetValue() $this->assertSame(-3, $r->getValue($obj)); } + + public function testFullPartialInitialization() + { + $counter = 0; + $initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + return 234; + }; + $instance = ChildTestClass::createLazyGhost([ + 'public' => $initializer, + 'publicReadonly' => $initializer, + "\0*\0protected" => $initializer, + "\0" => function ($obj, $defaults) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ] + $defaults; + }, + ]); + + $this->assertSame($instance, $instance->initializeLazyObject()); + $this->assertSame(345, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(6, ((array) $instance)["\0".ChildTestClass::class."\0private"]); + $this->assertSame(3, ((array) $instance)["\0".TestClass::class."\0private"]); + $this->assertSame(1000, $counter); + } + + public function testPartialInitializationFallback() + { + $counter = 0; + $instance = ChildTestClass::createLazyGhost([ + "\0" => function ($obj) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ]; + }, + ], []); + + $this->assertSame(345, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(567, ((array) $instance)["\0*\0protected"]); + $this->assertSame(1000, $counter); + } + + public function testFullInitializationAfterPartialInitialization() + { + $counter = 0; + $initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 234; + }; + $instance = ChildTestClass::createLazyGhost([ + 'public' => $initializer, + 'publicReadonly' => $initializer, + "\0*\0protected" => $initializer, + "\0" => function ($obj, $defaults) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ] + $defaults; + }, + ]); + + $this->assertSame(234, $instance->public); + $this->assertSame($instance, $instance->initializeLazyObject()); + $this->assertSame(234, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(6, ((array) $instance)["\0".ChildTestClass::class."\0private"]); + $this->assertSame(3, ((array) $instance)["\0".TestClass::class."\0private"]); + $this->assertSame(1001, $counter); + } } diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index c44c3b9cac6be..04497f02b34ae 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\VarExporter\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; @@ -237,9 +238,9 @@ public function setFoo($foo): static */ public function testReadOnlyClass() { - $proxy = $this->createLazyProxy(ReadOnlyClass::class, fn () => new ReadOnlyClass(123)); - - $this->assertSame(123, $proxy->foo); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot generate lazy proxy: class "Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass" is read-only.'); + $this->createLazyProxy(ReadOnlyClass::class, fn () => new ReadOnlyClass(123)); } public function testLazyDecoratorClass() @@ -249,9 +250,6 @@ public function testLazyDecoratorClass() createLazyProxy as private; } - private int $lazyObjectId; - private parent $lazyObjectReal; - public function __construct() { self::createLazyProxy(fn () => new TestClass((object) ['foo' => 123]), $this); diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index 455a7b974ec55..260980d94f870 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -66,9 +66,6 @@ public function testGenerateLazyProxy() { use \Symfony\Component\VarExporter\LazyProxyTrait; - private int $lazyObjectId; - private parent $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null], @@ -119,9 +116,6 @@ public function testGenerateLazyProxyForInterfaces() { use \Symfony\Component\VarExporter\LazyProxyTrait; - private int $lazyObjectId; - private \Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface1&\Symfony\Component\VarExporter\Tests\TestForProxyHelperInterface2 $lazyObjectReal; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ 'lazyObjectReal' => [self::class, 'lazyObjectReal', null], "\0".self::class."\0lazyObjectReal" => [self::class, 'lazyObjectReal', null],