diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 5bc2fcc022bb..a6a4d2e307dc 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -4,6 +4,7 @@ UPGRADE FROM 4.2 to 4.3 BrowserKit ---------- + * Renamed `Client` to `AbstractBrowser` * Marked `Response` final. * Deprecated `Response::buildHeader()` * Deprecated `Response::getStatus()`, use `Response::getStatusCode()` instead @@ -51,6 +52,11 @@ HttpFoundation * The `FileinfoMimeTypeGuesser` class has been deprecated, use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead. +HttpKernel +---------- + + * renamed `Client` to `HttpKernelBrowser` + Messenger --------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 39d1ab4303ab..1805e312e474 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -4,6 +4,7 @@ UPGRADE FROM 4.x to 5.0 BrowserKit ---------- + * Removed `Client`, use `AbstractBrowser` instead * Removed the possibility to extend `Response` by making it final. * Removed `Response::buildHeader()` * Removed `Response::getStatus()`, use `Response::getStatusCode()` instead @@ -199,6 +200,7 @@ HttpFoundation HttpKernel ---------- + * Removed `Client`, use `HttpKernelBrowser` instead * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been removed * The `KernelInterface::getName()` and the `kernel.name` parameter have been removed * Removed the first and second constructor argument of `ConfigDataCollector` diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 4fc04f0671b6..7ecd3acd76d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.3.0 ----- + * renamed `Client` to `KernelBrowser` * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will be mandatory in 5.0. * Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead diff --git a/src/Symfony/Bundle/FrameworkBundle/Client.php b/src/Symfony/Bundle/FrameworkBundle/Client.php index 6473f97584f3..e36f0cac6860 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Client.php +++ b/src/Symfony/Bundle/FrameworkBundle/Client.php @@ -11,196 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle; -use Symfony\Component\BrowserKit\CookieJar; -use Symfony\Component\BrowserKit\History; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Client as BaseClient; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', Client::class, KernelBrowser::class), E_USER_DEPRECATED); -/** - * Client simulates a browser and makes requests to a Kernel object. - * - * @author Fabien Potencier - */ -class Client extends BaseClient +class Client extends KernelBrowser { - private $hasPerformedRequest = false; - private $profiler = false; - private $reboot = true; - - /** - * {@inheritdoc} - */ - public function __construct(KernelInterface $kernel, array $server = [], History $history = null, CookieJar $cookieJar = null) - { - parent::__construct($kernel, $server, $history, $cookieJar); - } - - /** - * Returns the container. - * - * @return ContainerInterface|null Returns null when the Kernel has been shutdown or not started yet - */ - public function getContainer() - { - return $this->kernel->getContainer(); - } - - /** - * Returns the kernel. - * - * @return KernelInterface - */ - public function getKernel() - { - return $this->kernel; - } - - /** - * Gets the profile associated with the current Response. - * - * @return HttpProfile|false A Profile instance - */ - public function getProfile() - { - if (!$this->kernel->getContainer()->has('profiler')) { - return false; - } - - return $this->kernel->getContainer()->get('profiler')->loadProfileFromResponse($this->response); - } - - /** - * Enables the profiler for the very next request. - * - * If the profiler is not enabled, the call to this method does nothing. - */ - public function enableProfiler() - { - if ($this->kernel->getContainer()->has('profiler')) { - $this->profiler = true; - } - } - - /** - * Disables kernel reboot between requests. - * - * By default, the Client reboots the Kernel for each request. This method - * allows to keep the same kernel across requests. - */ - public function disableReboot() - { - $this->reboot = false; - } - - /** - * Enables kernel reboot between requests. - */ - public function enableReboot() - { - $this->reboot = true; - } - - /** - * {@inheritdoc} - * - * @param Request $request A Request instance - * - * @return Response A Response instance - */ - protected function doRequest($request) - { - // avoid shutting down the Kernel if no request has been performed yet - // WebTestCase::createClient() boots the Kernel but do not handle a request - if ($this->hasPerformedRequest && $this->reboot) { - $this->kernel->shutdown(); - } else { - $this->hasPerformedRequest = true; - } - - if ($this->profiler) { - $this->profiler = false; - - $this->kernel->boot(); - $this->kernel->getContainer()->get('profiler')->enable(); - } - - return parent::doRequest($request); - } - - /** - * {@inheritdoc} - * - * @param Request $request A Request instance - * - * @return Response A Response instance - */ - protected function doRequestInProcess($request) - { - $response = parent::doRequestInProcess($request); - - $this->profiler = false; - - return $response; - } - - /** - * Returns the script to execute when the request must be insulated. - * - * It assumes that the autoloader is named 'autoload.php' and that it is - * stored in the same directory as the kernel (this is the case for the - * Symfony Standard Edition). If this is not your case, create your own - * client and override this method. - * - * @param Request $request A Request instance - * - * @return string The script content - */ - protected function getScript($request) - { - $kernel = var_export(serialize($this->kernel), true); - $request = var_export(serialize($request), true); - $errorReporting = error_reporting(); - - $requires = ''; - foreach (get_declared_classes() as $class) { - if (0 === strpos($class, 'ComposerAutoloaderInit')) { - $r = new \ReflectionClass($class); - $file = \dirname(\dirname($r->getFileName())).'/autoload.php'; - if (file_exists($file)) { - $requires .= 'require_once '.var_export($file, true).";\n"; - } - } - } - - if (!$requires) { - throw new \RuntimeException('Composer autoloader not found.'); - } - - $requires .= 'require_once '.var_export((new \ReflectionObject($this->kernel))->getFileName(), true).";\n"; - - $profilerCode = ''; - if ($this->profiler) { - $profilerCode = '$kernel->getContainer()->get(\'profiler\')->enable();'; - } - - $code = <<boot(); -$profilerCode - -\$request = unserialize($request); -EOF; - - return $code.$this->getHandleScript(); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index a38518b2e922..cb9a06218bee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -22,7 +22,7 @@ use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; use Symfony\Bundle\FullStack; -use Symfony\Component\BrowserKit\Client; +use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -207,7 +207,7 @@ public function load(array $configs, ContainerBuilder $container) if (!empty($config['test'])) { $loader->load('test.xml'); - if (!class_exists(Client::class)) { + if (!class_exists(AbstractBrowser::class)) { $container->removeDefinition('test.client'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php new file mode 100644 index 000000000000..b86c1b5c3464 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle; + +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelBrowser; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; + +/** + * Client simulates a browser and makes requests to a Kernel object. + * + * @author Fabien Potencier + */ +class KernelBrowser extends HttpKernelBrowser +{ + private $hasPerformedRequest = false; + private $profiler = false; + private $reboot = true; + + /** + * {@inheritdoc} + */ + public function __construct(KernelInterface $kernel, array $server = [], History $history = null, CookieJar $cookieJar = null) + { + parent::__construct($kernel, $server, $history, $cookieJar); + } + + /** + * Returns the container. + * + * @return ContainerInterface|null Returns null when the Kernel has been shutdown or not started yet + */ + public function getContainer() + { + return $this->kernel->getContainer(); + } + + /** + * Returns the kernel. + * + * @return KernelInterface + */ + public function getKernel() + { + return $this->kernel; + } + + /** + * Gets the profile associated with the current Response. + * + * @return HttpProfile|false A Profile instance + */ + public function getProfile() + { + if (!$this->kernel->getContainer()->has('profiler')) { + return false; + } + + return $this->kernel->getContainer()->get('profiler')->loadProfileFromResponse($this->response); + } + + /** + * Enables the profiler for the very next request. + * + * If the profiler is not enabled, the call to this method does nothing. + */ + public function enableProfiler() + { + if ($this->kernel->getContainer()->has('profiler')) { + $this->profiler = true; + } + } + + /** + * Disables kernel reboot between requests. + * + * By default, the Client reboots the Kernel for each request. This method + * allows to keep the same kernel across requests. + */ + public function disableReboot() + { + $this->reboot = false; + } + + /** + * Enables kernel reboot between requests. + */ + public function enableReboot() + { + $this->reboot = true; + } + + /** + * {@inheritdoc} + * + * @param Request $request A Request instance + * + * @return Response A Response instance + */ + protected function doRequest($request) + { + // avoid shutting down the Kernel if no request has been performed yet + // WebTestCase::createClient() boots the Kernel but do not handle a request + if ($this->hasPerformedRequest && $this->reboot) { + $this->kernel->shutdown(); + } else { + $this->hasPerformedRequest = true; + } + + if ($this->profiler) { + $this->profiler = false; + + $this->kernel->boot(); + $this->kernel->getContainer()->get('profiler')->enable(); + } + + return parent::doRequest($request); + } + + /** + * {@inheritdoc} + * + * @param Request $request A Request instance + * + * @return Response A Response instance + */ + protected function doRequestInProcess($request) + { + $response = parent::doRequestInProcess($request); + + $this->profiler = false; + + return $response; + } + + /** + * Returns the script to execute when the request must be insulated. + * + * It assumes that the autoloader is named 'autoload.php' and that it is + * stored in the same directory as the kernel (this is the case for the + * Symfony Standard Edition). If this is not your case, create your own + * client and override this method. + * + * @param Request $request A Request instance + * + * @return string The script content + */ + protected function getScript($request) + { + $kernel = var_export(serialize($this->kernel), true); + $request = var_export(serialize($request), true); + $errorReporting = error_reporting(); + + $requires = ''; + foreach (get_declared_classes() as $class) { + if (0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $file = \dirname(\dirname($r->getFileName())).'/autoload.php'; + if (file_exists($file)) { + $requires .= 'require_once '.var_export($file, true).";\n"; + } + } + } + + if (!$requires) { + throw new \RuntimeException('Composer autoloader not found.'); + } + + $requires .= 'require_once '.var_export((new \ReflectionObject($this->kernel))->getFileName(), true).";\n"; + + $profilerCode = ''; + if ($this->profiler) { + $profilerCode = '$kernel->getContainer()->get(\'profiler\')->enable();'; + } + + $code = <<boot(); +$profilerCode + +\$request = unserialize($request); +EOF; + + return $code.$this->getHandleScript(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml index d47bc769d365..ef571fdbc674 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml @@ -11,7 +11,7 @@ - + %test.client.parameters% diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index e56e938dd994..9fc654ae935b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Test; -use Symfony\Bundle\FrameworkBundle\Client; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; /** @@ -22,12 +22,12 @@ abstract class WebTestCase extends KernelTestCase { /** - * Creates a Client. + * Creates a KernelBrowser. * * @param array $options An array of options to pass to the createKernel method * @param array $server An array of server parameters * - * @return Client A Client instance + * @return KernelBrowser A KernelBrowser instance */ protected static function createClient(array $options = [], array $server = []) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php similarity index 87% rename from src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php index ba253d1d1f51..7fb27a63ee72 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php @@ -11,18 +11,18 @@ namespace Symfony\Bundle\FrameworkBundle\Tests; -use Symfony\Bundle\FrameworkBundle\Client; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Tests\Functional\WebTestCase; use Symfony\Component\HttpFoundation\Response; -class ClientTest extends WebTestCase +class KernelBrowserTest extends WebTestCase { public function testRebootKernelBetweenRequests() { $mock = $this->getKernelMock(); $mock->expects($this->once())->method('shutdown'); - $client = new Client($mock); + $client = new KernelBrowser($mock); $client->request('GET', '/'); $client->request('GET', '/'); } @@ -32,7 +32,7 @@ public function testDisabledRebootKernel() $mock = $this->getKernelMock(); $mock->expects($this->never())->method('shutdown'); - $client = new Client($mock); + $client = new KernelBrowser($mock); $client->disableReboot(); $client->request('GET', '/'); $client->request('GET', '/'); @@ -43,7 +43,7 @@ public function testEnableRebootKernel() $mock = $this->getKernelMock(); $mock->expects($this->once())->method('shutdown'); - $client = new Client($mock); + $client = new KernelBrowser($mock); $client->disableReboot(); $client->request('GET', '/'); $client->request('GET', '/'); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 764a0c0d2002..cdf40d12a21f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -34,7 +34,7 @@ "doctrine/cache": "~1.0", "fig/link-util": "^1.0", "symfony/asset": "~3.4|~4.0", - "symfony/browser-kit": "~3.4|~4.0", + "symfony/browser-kit": "^4.3", "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~3.4|~4.0", "symfony/dom-crawler": "~3.4|~4.0", @@ -67,6 +67,7 @@ "phpdocumentor/type-resolver": "<0.2.1", "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/asset": "<3.4", + "symfony/browser-kit": "<4.3", "symfony/console": "<3.4", "symfony/dotenv": "<4.2", "symfony/form": "<4.3", diff --git a/src/Symfony/Component/BrowserKit/AbstractBrowser.php b/src/Symfony/Component/BrowserKit/AbstractBrowser.php new file mode 100644 index 000000000000..fd6dc27a0d4f --- /dev/null +++ b/src/Symfony/Component/BrowserKit/AbstractBrowser.php @@ -0,0 +1,739 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\BrowserKit; + +use Symfony\Component\BrowserKit\Exception\BadMethodCallException; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Form; +use Symfony\Component\DomCrawler\Link; +use Symfony\Component\Process\PhpProcess; + +/** + * Simulates a browser. + * + * To make the actual request, you need to implement the doRequest() method. + * + * HttpBrowser is an implementation that uses the HttpClient component + * to make real HTTP requests. + * + * If you want to be able to run requests in their own process (insulated flag), + * you need to also implement the getScript() method. + * + * @author Fabien Potencier + */ +abstract class AbstractBrowser +{ + protected $history; + protected $cookieJar; + protected $server = []; + protected $internalRequest; + protected $request; + protected $internalResponse; + protected $response; + protected $crawler; + protected $insulated = false; + protected $redirect; + protected $followRedirects = true; + protected $followMetaRefresh = false; + + private $maxRedirects = -1; + private $redirectCount = 0; + private $redirects = []; + private $isMainRequest = true; + + /** + * @param array $server The server parameters (equivalent of $_SERVER) + * @param History $history A History instance to store the browser history + * @param CookieJar $cookieJar A CookieJar instance to store the cookies + */ + public function __construct(array $server = [], History $history = null, CookieJar $cookieJar = null) + { + $this->setServerParameters($server); + $this->history = $history ?: new History(); + $this->cookieJar = $cookieJar ?: new CookieJar(); + } + + /** + * Sets whether to automatically follow redirects or not. + * + * @param bool $followRedirect Whether to follow redirects + */ + public function followRedirects($followRedirect = true) + { + $this->followRedirects = (bool) $followRedirect; + } + + /** + * Sets whether to automatically follow meta refresh redirects or not. + */ + public function followMetaRefresh(bool $followMetaRefresh = true) + { + $this->followMetaRefresh = $followMetaRefresh; + } + + /** + * Returns whether client automatically follows redirects or not. + * + * @return bool + */ + public function isFollowingRedirects() + { + return $this->followRedirects; + } + + /** + * Sets the maximum number of redirects that crawler can follow. + * + * @param int $maxRedirects + */ + public function setMaxRedirects($maxRedirects) + { + $this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects; + $this->followRedirects = -1 != $this->maxRedirects; + } + + /** + * Returns the maximum number of redirects that crawler can follow. + * + * @return int + */ + public function getMaxRedirects() + { + return $this->maxRedirects; + } + + /** + * Sets the insulated flag. + * + * @param bool $insulated Whether to insulate the requests or not + * + * @throws \RuntimeException When Symfony Process Component is not installed + */ + public function insulate($insulated = true) + { + if ($insulated && !class_exists('Symfony\\Component\\Process\\Process')) { + throw new \LogicException('Unable to isolate requests as the Symfony Process Component is not installed.'); + } + + $this->insulated = (bool) $insulated; + } + + /** + * Sets server parameters. + * + * @param array $server An array of server parameters + */ + public function setServerParameters(array $server) + { + $this->server = array_merge([ + 'HTTP_USER_AGENT' => 'Symfony BrowserKit', + ], $server); + } + + /** + * Sets single server parameter. + * + * @param string $key A key of the parameter + * @param string $value A value of the parameter + */ + public function setServerParameter($key, $value) + { + $this->server[$key] = $value; + } + + /** + * Gets single server parameter for specified key. + * + * @param string $key A key of the parameter to get + * @param string $default A default value when key is undefined + * + * @return string A value of the parameter + */ + public function getServerParameter($key, $default = '') + { + return isset($this->server[$key]) ? $this->server[$key] : $default; + } + + public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler + { + $this->setServerParameter('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'); + + try { + return $this->request($method, $uri, $parameters, $files, $server, $content, $changeHistory); + } finally { + unset($this->server['HTTP_X_REQUESTED_WITH']); + } + } + + /** + * Returns the History instance. + * + * @return History A History instance + */ + public function getHistory() + { + return $this->history; + } + + /** + * Returns the CookieJar instance. + * + * @return CookieJar A CookieJar instance + */ + public function getCookieJar() + { + return $this->cookieJar; + } + + /** + * Returns the current Crawler instance. + * + * @return Crawler A Crawler instance + */ + public function getCrawler() + { + if (null === $this->crawler) { + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', __METHOD__), E_USER_DEPRECATED); + // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->crawler; + } + + /** + * Returns the current BrowserKit Response instance. + * + * @return Response A BrowserKit Response instance + */ + public function getInternalResponse() + { + if (null === $this->internalResponse) { + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', __METHOD__), E_USER_DEPRECATED); + // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->internalResponse; + } + + /** + * Returns the current origin response instance. + * + * The origin response is the response instance that is returned + * by the code that handles requests. + * + * @return object A response instance + * + * @see doRequest() + */ + public function getResponse() + { + if (null === $this->response) { + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', __METHOD__), E_USER_DEPRECATED); + // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->response; + } + + /** + * Returns the current BrowserKit Request instance. + * + * @return Request A BrowserKit Request instance + */ + public function getInternalRequest() + { + if (null === $this->internalRequest) { + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', __METHOD__), E_USER_DEPRECATED); + // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->internalRequest; + } + + /** + * Returns the current origin Request instance. + * + * The origin request is the request instance that is sent + * to the code that handles requests. + * + * @return object A Request instance + * + * @see doRequest() + */ + public function getRequest() + { + if (null === $this->request) { + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', __METHOD__), E_USER_DEPRECATED); + // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->request; + } + + /** + * Clicks on a given link. + * + * @return Crawler + */ + public function click(Link $link) + { + if ($link instanceof Form) { + return $this->submit($link); + } + + return $this->request($link->getMethod(), $link->getUri()); + } + + /** + * Clicks the first link (or clickable image) that contains the given text. + * + * @param string $linkText The text of the link or the alt attribute of the clickable image + */ + public function clickLink(string $linkText): Crawler + { + if (null === $this->crawler) { + throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); + } + + return $this->click($this->crawler->selectLink($linkText)->link()); + } + + /** + * Submits a form. + * + * @param Form $form A Form instance + * @param array $values An array of form field values + * @param array $serverParameters An array of server parameters + * + * @return Crawler + */ + public function submit(Form $form, array $values = []/*, array $serverParameters = []*/) + { + if (\func_num_args() < 3 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "array $serverParameters = []" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + } + + $form->setValues($values); + $serverParameters = 2 < \func_num_args() ? func_get_arg(2) : []; + + return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles(), $serverParameters); + } + + /** + * Finds the first form that contains a button with the given content and + * uses it to submit the given form field values. + * + * @param string $button The text content, id, value or name of the form