RegisterControllerArgumentLocatorsPassTest.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
  11. use PHPUnit\Framework\TestCase;
  12. use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
  13. use Symfony\Component\DependencyInjection\Attribute\Target;
  14. use Symfony\Component\DependencyInjection\ChildDefinition;
  15. use Symfony\Component\DependencyInjection\ContainerAwareInterface;
  16. use Symfony\Component\DependencyInjection\ContainerAwareTrait;
  17. use Symfony\Component\DependencyInjection\ContainerBuilder;
  18. use Symfony\Component\DependencyInjection\ContainerInterface;
  19. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  20. use Symfony\Component\DependencyInjection\Reference;
  21. use Symfony\Component\DependencyInjection\ServiceLocator;
  22. use Symfony\Component\DependencyInjection\TypedReference;
  23. use Symfony\Component\HttpFoundation\Response;
  24. use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
  25. use Symfony\Component\HttpKernel\Tests\Fixtures\Suit;
  26. class RegisterControllerArgumentLocatorsPassTest extends TestCase
  27. {
  28. public function testInvalidClass()
  29. {
  30. $this->expectException(InvalidArgumentException::class);
  31. $this->expectExceptionMessage('Class "Symfony\Component\HttpKernel\Tests\DependencyInjection\NotFound" used for service "foo" cannot be found.');
  32. $container = new ContainerBuilder();
  33. $container->register('argument_resolver.service')->addArgument([]);
  34. $container->register('foo', NotFound::class)
  35. ->addTag('controller.service_arguments')
  36. ;
  37. $pass = new RegisterControllerArgumentLocatorsPass();
  38. $pass->process($container);
  39. }
  40. public function testNoAction()
  41. {
  42. $this->expectException(InvalidArgumentException::class);
  43. $this->expectExceptionMessage('Missing "action" attribute on tag "controller.service_arguments" {"argument":"bar"} for service "foo".');
  44. $container = new ContainerBuilder();
  45. $container->register('argument_resolver.service')->addArgument([]);
  46. $container->register('foo', RegisterTestController::class)
  47. ->addTag('controller.service_arguments', ['argument' => 'bar'])
  48. ;
  49. $pass = new RegisterControllerArgumentLocatorsPass();
  50. $pass->process($container);
  51. }
  52. public function testNoArgument()
  53. {
  54. $this->expectException(InvalidArgumentException::class);
  55. $this->expectExceptionMessage('Missing "argument" attribute on tag "controller.service_arguments" {"action":"fooAction"} for service "foo".');
  56. $container = new ContainerBuilder();
  57. $container->register('argument_resolver.service')->addArgument([]);
  58. $container->register('foo', RegisterTestController::class)
  59. ->addTag('controller.service_arguments', ['action' => 'fooAction'])
  60. ;
  61. $pass = new RegisterControllerArgumentLocatorsPass();
  62. $pass->process($container);
  63. }
  64. public function testNoService()
  65. {
  66. $this->expectException(InvalidArgumentException::class);
  67. $this->expectExceptionMessage('Missing "id" attribute on tag "controller.service_arguments" {"action":"fooAction","argument":"bar"} for service "foo".');
  68. $container = new ContainerBuilder();
  69. $container->register('argument_resolver.service')->addArgument([]);
  70. $container->register('foo', RegisterTestController::class)
  71. ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar'])
  72. ;
  73. $pass = new RegisterControllerArgumentLocatorsPass();
  74. $pass->process($container);
  75. }
  76. public function testInvalidMethod()
  77. {
  78. $this->expectException(InvalidArgumentException::class);
  79. $this->expectExceptionMessage('Invalid "action" attribute on tag "controller.service_arguments" for service "foo": no public "barAction()" method found on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".');
  80. $container = new ContainerBuilder();
  81. $container->register('argument_resolver.service')->addArgument([]);
  82. $container->register('foo', RegisterTestController::class)
  83. ->addTag('controller.service_arguments', ['action' => 'barAction', 'argument' => 'bar', 'id' => 'bar_service'])
  84. ;
  85. $pass = new RegisterControllerArgumentLocatorsPass();
  86. $pass->process($container);
  87. }
  88. public function testInvalidArgument()
  89. {
  90. $this->expectException(InvalidArgumentException::class);
  91. $this->expectExceptionMessage('Invalid "controller.service_arguments" tag for service "foo": method "fooAction()" has no "baz" argument on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".');
  92. $container = new ContainerBuilder();
  93. $container->register('argument_resolver.service')->addArgument([]);
  94. $container->register('foo', RegisterTestController::class)
  95. ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'baz', 'id' => 'bar'])
  96. ;
  97. $pass = new RegisterControllerArgumentLocatorsPass();
  98. $pass->process($container);
  99. }
  100. public function testAllActions()
  101. {
  102. $container = new ContainerBuilder();
  103. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  104. $container->register('foo', RegisterTestController::class)
  105. ->addTag('controller.service_arguments')
  106. ;
  107. $pass = new RegisterControllerArgumentLocatorsPass();
  108. $pass->process($container);
  109. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  110. $this->assertEquals(['foo::fooAction'], array_keys($locator));
  111. $this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']);
  112. $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
  113. $this->assertSame(ServiceLocator::class, $locator->getClass());
  114. $this->assertFalse($locator->isPublic());
  115. $expected = ['bar' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'bar'))];
  116. $this->assertEquals($expected, $locator->getArgument(0));
  117. }
  118. public function testExplicitArgument()
  119. {
  120. $container = new ContainerBuilder();
  121. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  122. $container->register('foo', RegisterTestController::class)
  123. ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => 'bar'])
  124. ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => 'baz']) // should be ignored, the first wins
  125. ;
  126. $pass = new RegisterControllerArgumentLocatorsPass();
  127. $pass->process($container);
  128. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  129. $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
  130. $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))];
  131. $this->assertEquals($expected, $locator->getArgument(0));
  132. }
  133. public function testOptionalArgument()
  134. {
  135. $container = new ContainerBuilder();
  136. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  137. $container->register('foo', RegisterTestController::class)
  138. ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => '?bar'])
  139. ;
  140. $pass = new RegisterControllerArgumentLocatorsPass();
  141. $pass->process($container);
  142. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  143. $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
  144. $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))];
  145. $this->assertEquals($expected, $locator->getArgument(0));
  146. }
  147. public function testSkipSetContainer()
  148. {
  149. $container = new ContainerBuilder();
  150. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  151. $container->register('foo', ContainerAwareRegisterTestController::class)
  152. ->addTag('controller.service_arguments');
  153. $pass = new RegisterControllerArgumentLocatorsPass();
  154. $pass->process($container);
  155. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  156. $this->assertSame(['foo::fooAction'], array_keys($locator));
  157. }
  158. public function testExceptionOnNonExistentTypeHint()
  159. {
  160. $this->expectException(\RuntimeException::class);
  161. $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClass". Did you forget to add a use statement?');
  162. $container = new ContainerBuilder();
  163. $container->register('argument_resolver.service')->addArgument([]);
  164. $container->register('foo', NonExistentClassController::class)
  165. ->addTag('controller.service_arguments');
  166. $pass = new RegisterControllerArgumentLocatorsPass();
  167. $pass->process($container);
  168. $error = $container->getDefinition('argument_resolver.service')->getArgument(0);
  169. $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0];
  170. $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0];
  171. $container->get($error);
  172. }
  173. public function testExceptionOnNonExistentTypeHintDifferentNamespace()
  174. {
  175. $this->expectException(\RuntimeException::class);
  176. $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassDifferentNamespaceController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Acme\NonExistentClass".');
  177. $container = new ContainerBuilder();
  178. $container->register('argument_resolver.service')->addArgument([]);
  179. $container->register('foo', NonExistentClassDifferentNamespaceController::class)
  180. ->addTag('controller.service_arguments');
  181. $pass = new RegisterControllerArgumentLocatorsPass();
  182. $pass->process($container);
  183. $error = $container->getDefinition('argument_resolver.service')->getArgument(0);
  184. $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0];
  185. $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0];
  186. $container->get($error);
  187. }
  188. public function testNoExceptionOnNonExistentTypeHintOptionalArg()
  189. {
  190. $container = new ContainerBuilder();
  191. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  192. $container->register('foo', NonExistentClassOptionalController::class)
  193. ->addTag('controller.service_arguments');
  194. $pass = new RegisterControllerArgumentLocatorsPass();
  195. $pass->process($container);
  196. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  197. $this->assertEqualsCanonicalizing(['foo::barAction', 'foo::fooAction'], array_keys($locator));
  198. }
  199. public function testArgumentWithNoTypeHintIsOk()
  200. {
  201. $container = new ContainerBuilder();
  202. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  203. $container->register('foo', ArgumentWithoutTypeController::class)
  204. ->addTag('controller.service_arguments');
  205. $pass = new RegisterControllerArgumentLocatorsPass();
  206. $pass->process($container);
  207. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  208. $this->assertEmpty(array_keys($locator));
  209. }
  210. public function testControllersAreMadePublic()
  211. {
  212. $container = new ContainerBuilder();
  213. $container->register('argument_resolver.service')->addArgument([]);
  214. $container->register('foo', ArgumentWithoutTypeController::class)
  215. ->setPublic(false)
  216. ->addTag('controller.service_arguments');
  217. $pass = new RegisterControllerArgumentLocatorsPass();
  218. $pass->process($container);
  219. $this->assertTrue($container->getDefinition('foo')->isPublic());
  220. }
  221. /**
  222. * @dataProvider provideBindings
  223. */
  224. public function testBindings($bindingName)
  225. {
  226. $container = new ContainerBuilder();
  227. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  228. $container->register('foo', RegisterTestController::class)
  229. ->setBindings([$bindingName => new Reference('foo')])
  230. ->addTag('controller.service_arguments');
  231. $pass = new RegisterControllerArgumentLocatorsPass();
  232. $pass->process($container);
  233. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  234. $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
  235. $expected = ['bar' => new ServiceClosureArgument(new Reference('foo'))];
  236. $this->assertEquals($expected, $locator->getArgument(0));
  237. }
  238. public static function provideBindings()
  239. {
  240. return [
  241. [ControllerDummy::class.'$bar'],
  242. [ControllerDummy::class],
  243. ['$bar'],
  244. ];
  245. }
  246. /**
  247. * @dataProvider provideBindScalarValueToControllerArgument
  248. */
  249. public function testBindScalarValueToControllerArgument($bindingKey)
  250. {
  251. $container = new ContainerBuilder();
  252. $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]);
  253. $container->register('foo', ArgumentWithoutTypeController::class)
  254. ->setBindings([$bindingKey => '%foo%'])
  255. ->addTag('controller.service_arguments');
  256. $container->setParameter('foo', 'foo_val');
  257. $pass = new RegisterControllerArgumentLocatorsPass();
  258. $pass->process($container);
  259. $locatorId = (string) $resolver->getArgument(0);
  260. $container->getDefinition($locatorId)->setPublic(true);
  261. $container->compile();
  262. $locator = $container->get($locatorId);
  263. $this->assertSame('foo_val', $locator->get('foo::fooAction')->get('someArg'));
  264. }
  265. public static function provideBindScalarValueToControllerArgument()
  266. {
  267. yield ['$someArg'];
  268. yield ['string $someArg'];
  269. }
  270. public function testBindingsOnChildDefinitions()
  271. {
  272. $container = new ContainerBuilder();
  273. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  274. $container->register('parent', ArgumentWithoutTypeController::class);
  275. $container->setDefinition('child', (new ChildDefinition('parent'))
  276. ->setBindings(['$someArg' => new Reference('parent')])
  277. ->addTag('controller.service_arguments')
  278. );
  279. $pass = new RegisterControllerArgumentLocatorsPass();
  280. $pass->process($container);
  281. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  282. $this->assertInstanceOf(ServiceClosureArgument::class, $locator['child::fooAction']);
  283. $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0])->getArgument(0);
  284. $this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']);
  285. $this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]);
  286. }
  287. public function testNotTaggedControllerServiceReceivesLocatorArgument()
  288. {
  289. $container = new ContainerBuilder();
  290. $container->register('argument_resolver.not_tagged_controller')->addArgument([]);
  291. $pass = new RegisterControllerArgumentLocatorsPass();
  292. $pass->process($container);
  293. $locatorArgument = $container->getDefinition('argument_resolver.not_tagged_controller')->getArgument(0);
  294. $this->assertInstanceOf(Reference::class, $locatorArgument);
  295. }
  296. public function testAlias()
  297. {
  298. $container = new ContainerBuilder();
  299. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  300. $container->register('foo', RegisterTestController::class)
  301. ->addTag('controller.service_arguments');
  302. $container->setAlias(RegisterTestController::class, 'foo')->setPublic(true);
  303. $pass = new RegisterControllerArgumentLocatorsPass();
  304. $pass->process($container);
  305. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  306. $this->assertEqualsCanonicalizing([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator));
  307. }
  308. /**
  309. * @requires PHP 8.1
  310. */
  311. public function testEnumArgumentIsIgnored()
  312. {
  313. $container = new ContainerBuilder();
  314. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  315. $container->register('foo', NonNullableEnumArgumentWithDefaultController::class)
  316. ->addTag('controller.service_arguments')
  317. ;
  318. $pass = new RegisterControllerArgumentLocatorsPass();
  319. $pass->process($container);
  320. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  321. $this->assertEmpty(array_keys($locator), 'enum typed argument is ignored');
  322. }
  323. /**
  324. * @requires PHP 8
  325. */
  326. public function testBindWithTarget()
  327. {
  328. $container = new ContainerBuilder();
  329. $resolver = $container->register('argument_resolver.service')->addArgument([]);
  330. $container->register(ControllerDummy::class, 'bar');
  331. $container->register(ControllerDummy::class.' $imageStorage', 'baz');
  332. $container->register('foo', WithTarget::class)
  333. ->setBindings(['string $someApiKey' => new Reference('the_api_key')])
  334. ->addTag('controller.service_arguments');
  335. (new RegisterControllerArgumentLocatorsPass())->process($container);
  336. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  337. $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
  338. $expected = [
  339. 'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')),
  340. 'service1' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'imageStorage')),
  341. 'service2' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'service2')),
  342. ];
  343. $this->assertEquals($expected, $locator->getArgument(0));
  344. }
  345. public function testResponseArgumentIsIgnored()
  346. {
  347. $container = new ContainerBuilder();
  348. $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]);
  349. $container->register('foo', WithResponseArgument::class)
  350. ->addTag('controller.service_arguments');
  351. (new RegisterControllerArgumentLocatorsPass())->process($container);
  352. $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
  353. $this->assertEmpty(array_keys($locator), 'Response typed argument is ignored');
  354. }
  355. }
  356. class RegisterTestController
  357. {
  358. public function __construct(ControllerDummy $bar)
  359. {
  360. }
  361. public function fooAction(ControllerDummy $bar)
  362. {
  363. }
  364. protected function barAction(ControllerDummy $bar)
  365. {
  366. }
  367. }
  368. class ContainerAwareRegisterTestController implements ContainerAwareInterface
  369. {
  370. use ContainerAwareTrait;
  371. public function fooAction(ControllerDummy $bar)
  372. {
  373. }
  374. }
  375. class ControllerDummy
  376. {
  377. }
  378. class NonExistentClassController
  379. {
  380. public function fooAction(NonExistentClass $nonExistent)
  381. {
  382. }
  383. }
  384. class NonExistentClassDifferentNamespaceController
  385. {
  386. public function fooAction(\Acme\NonExistentClass $nonExistent)
  387. {
  388. }
  389. }
  390. class NonExistentClassOptionalController
  391. {
  392. public function fooAction(NonExistentClass $nonExistent = null)
  393. {
  394. }
  395. public function barAction(NonExistentClass $nonExistent = null, $bar)
  396. {
  397. }
  398. }
  399. class ArgumentWithoutTypeController
  400. {
  401. public function fooAction(string $someArg)
  402. {
  403. }
  404. }
  405. class NonNullableEnumArgumentWithDefaultController
  406. {
  407. public function fooAction(Suit $suit = Suit::Spades)
  408. {
  409. }
  410. }
  411. class WithTarget
  412. {
  413. public function fooAction(
  414. #[Target('some.api.key')]
  415. string $apiKey,
  416. #[Target('image.storage')]
  417. ControllerDummy $service1,
  418. ControllerDummy $service2
  419. ) {
  420. }
  421. }
  422. class WithResponseArgument
  423. {
  424. public function fooAction(Response $response, ?Response $nullableResponse)
  425. {
  426. }
  427. }