class AbstractBrowser
Simulates a browser.
To make the actual request, you need to implement the doRequest() method.
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 <fabien@symfony.com>
@template TRequest of object @template TResponse of object
Hierarchy
- class \Symfony\Component\BrowserKit\AbstractBrowser
Expanded class hierarchy of AbstractBrowser
4 files declare their use of AbstractBrowser
- BrowserCookieValueSame.php in vendor/
symfony/ browser-kit/ Test/ Constraint/ BrowserCookieValueSame.php - BrowserHasCookie.php in vendor/
symfony/ browser-kit/ Test/ Constraint/ BrowserHasCookie.php - BrowserKitDriver.php in vendor/
behat/ mink-browserkit-driver/ src/ BrowserKitDriver.php - HttpKernelBrowser.php in vendor/
symfony/ http-kernel/ HttpKernelBrowser.php
File
-
vendor/
symfony/ browser-kit/ AbstractBrowser.php, line 36
Namespace
Symfony\Component\BrowserKitView source
abstract class AbstractBrowser {
protected History $history;
protected CookieJar $cookieJar;
protected array $server = [];
protected Request $internalRequest;
/** @psalm-var TRequest */
protected object $request;
protected Response $internalResponse;
/** @psalm-var TResponse */
protected object $response;
protected Crawler $crawler;
protected bool $useHtml5Parser = true;
protected bool $insulated = false;
protected ?string $redirect;
protected bool $followRedirects = true;
protected bool $followMetaRefresh = false;
private int $maxRedirects = -1;
private int $redirectCount = 0;
private array $redirects = [];
private bool $isMainRequest = true;
/**
* @param array $server The server parameters (equivalent of $_SERVER)
*/
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.
*/
public function followRedirects(bool $followRedirects = true) : void {
$this->followRedirects = $followRedirects;
}
/**
* Sets whether to automatically follow meta refresh redirects or not.
*/
public function followMetaRefresh(bool $followMetaRefresh = true) : void {
$this->followMetaRefresh = $followMetaRefresh;
}
/**
* Returns whether client automatically follows redirects or not.
*/
public function isFollowingRedirects() : bool {
return $this->followRedirects;
}
/**
* Sets the maximum number of redirects that crawler can follow.
*/
public function setMaxRedirects(int $maxRedirects) : void {
$this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects;
$this->followRedirects = -1 !== $this->maxRedirects;
}
/**
* Returns the maximum number of redirects that crawler can follow.
*/
public function getMaxRedirects() : int {
return $this->maxRedirects;
}
/**
* Sets the insulated flag.
*
* @throws LogicException When Symfony Process Component is not installed
*/
public function insulate(bool $insulated = true) : void {
if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) {
throw new LogicException('Unable to isolate requests as the Symfony Process Component is not installed. Try running "composer require symfony/process".');
}
$this->insulated = $insulated;
}
/**
* Sets server parameters.
*/
public function setServerParameters(array $server) : void {
$this->server = array_merge([
'HTTP_USER_AGENT' => 'Symfony BrowserKit',
], $server);
}
/**
* Sets single server parameter.
*/
public function setServerParameter(string $key, string $value) : void {
$this->server[$key] = $value;
}
/**
* Gets single server parameter for specified key.
*/
public function getServerParameter(string $key, mixed $default = '') : mixed {
return $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']);
}
}
/**
* Converts the request parameters into a JSON string and uses it as request content.
*/
public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true) : Crawler {
$content = json_encode($parameters);
$this->setServerParameter('CONTENT_TYPE', 'application/json');
$this->setServerParameter('HTTP_ACCEPT', 'application/json');
try {
return $this->request($method, $uri, [], [], $server, $content, $changeHistory);
} finally {
unset($this->server['CONTENT_TYPE']);
unset($this->server['HTTP_ACCEPT']);
}
}
/**
* Returns the History instance.
*/
public function getHistory() : History {
return $this->history;
}
/**
* Returns the CookieJar instance.
*/
public function getCookieJar() : CookieJar {
return $this->cookieJar;
}
/**
* Returns the current Crawler instance.
*/
public function getCrawler() : Crawler {
return $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Sets whether parsing should be done using "masterminds/html5".
*
* @return $this
*/
public function useHtml5Parser(bool $useHtml5Parser) : static {
$this->useHtml5Parser = $useHtml5Parser;
return $this;
}
/**
* Returns the current BrowserKit Response instance.
*/
public function getInternalResponse() : Response {
return $this->internalResponse ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current origin response instance.
*
* The origin response is the response instance that is returned
* by the code that handles requests.
*
* @psalm-return TResponse
*
* @see doRequest()
*/
public function getResponse() : object {
return $this->response ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current BrowserKit Request instance.
*/
public function getInternalRequest() : Request {
return $this->internalRequest ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current origin Request instance.
*
* The origin request is the request instance that is sent
* to the code that handles requests.
*
* @psalm-return TRequest
*
* @see doRequest()
*/
public function getRequest() : object {
return $this->request ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Clicks on a given link.
*
* @param array $serverParameters An array of server parameters
*/
public function click(Link $link, array $serverParameters = []) : Crawler {
if ($link instanceof Form) {
return $this->submit($link, [], $serverParameters);
}
return $this->request($link->getMethod(), $link->getUri(), [], [], $serverParameters);
}
/**
* 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
* @param array $serverParameters An array of server parameters
*/
public function clickLink(string $linkText, array $serverParameters = []) : Crawler {
$crawler = $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
return $this->click($crawler->selectLink($linkText)
->link(), $serverParameters);
}
/**
* Submits a form.
*
* @param array $values An array of form field values
* @param array $serverParameters An array of server parameters
*/
public function submit(Form $form, array $values = [], array $serverParameters = []) : Crawler {
$form->setValues($values);
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 <button> or <input type="submit">
* @param array $fieldValues Use this syntax: ['my_form[name]' => '...', 'my_form[email]' => '...']
* @param string $method The HTTP method used to submit the form
* @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include an HTTP_ prefix as PHP does)
*/
public function submitForm(string $button, array $fieldValues = [], string $method = 'POST', array $serverParameters = []) : Crawler {
$crawler = $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
$buttonNode = $crawler->selectButton($button);
if (0 === $buttonNode->count()) {
throw new InvalidArgumentException(\sprintf('There is no button with "%s" as its content, id, value or name.', $button));
}
$form = $buttonNode->form($fieldValues, $method);
return $this->submit($form, [], $serverParameters);
}
/**
* Calls a URI.
*
* @param string $method The request method
* @param string $uri The URI to fetch
* @param array $parameters The Request parameters
* @param array $files The files
* @param array $server The server parameters (HTTP headers are referenced with an HTTP_ prefix as PHP does)
* @param string $content The raw body data
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
*/
public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], ?string $content = null, bool $changeHistory = true) : Crawler {
if ($this->isMainRequest) {
$this->redirectCount = 0;
}
else {
++$this->redirectCount;
}
$originalUri = $uri;
$uri = $this->getAbsoluteUri($uri);
$server = array_merge($this->server, $server);
if (!empty($server['HTTP_HOST']) && !parse_url($originalUri, \PHP_URL_HOST)) {
$uri = preg_replace('{^(https?\\://)' . preg_quote($this->extractHost($uri)) . '}', '${1}' . $server['HTTP_HOST'], $uri);
}
if (isset($server['HTTPS']) && !parse_url($originalUri, \PHP_URL_SCHEME)) {
$uri = preg_replace('{^' . parse_url($uri, \PHP_URL_SCHEME) . '}', $server['HTTPS'] ? 'https' : 'http', $uri);
}
if (!isset($server['HTTP_REFERER']) && !$this->history
->isEmpty()) {
$server['HTTP_REFERER'] = $this->history
->current()
->getUri();
}
if (empty($server['HTTP_HOST'])) {
$server['HTTP_HOST'] = $this->extractHost($uri);
}
$server['HTTPS'] = 'https' === parse_url($uri, \PHP_URL_SCHEME);
$this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar
->allValues($uri), $server, $content);
$this->request = $this->filterRequest($this->internalRequest);
if (true === $changeHistory) {
$this->history
->add($this->internalRequest);
}
if ($this->insulated) {
$this->response = $this->doRequestInProcess($this->request);
}
else {
$this->response = $this->doRequest($this->request);
}
$this->internalResponse = $this->filterResponse($this->response);
$this->cookieJar
->updateFromResponse($this->internalResponse, $uri);
$status = $this->internalResponse
->getStatusCode();
if ($status >= 300 && $status < 400) {
$this->redirect = $this->internalResponse
->getHeader('Location');
}
else {
$this->redirect = null;
}
if ($this->followRedirects && $this->redirect) {
$this->redirects[serialize($this->history
->current())] = true;
return $this->crawler = $this->followRedirect();
}
$this->crawler = $this->createCrawlerFromContent($this->internalRequest
->getUri(), $this->internalResponse
->getContent(), $this->internalResponse
->getHeader('Content-Type') ?? '');
// Check for meta refresh redirect
if ($this->followMetaRefresh && null !== ($redirect = $this->getMetaRefreshUrl())) {
$this->redirect = $redirect;
$this->redirects[serialize($this->history
->current())] = true;
$this->crawler = $this->followRedirect();
}
return $this->crawler;
}
/**
* Makes a request in another process.
*
* @psalm-param TRequest $request
*
* @return object
*
* @psalm-return TResponse
*
* @throws \RuntimeException When processing returns exit code
*/
protected function doRequestInProcess(object $request) {
$deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
putenv('SYMFONY_DEPRECATIONS_SERIALIZE=' . $deprecationsFile);
$_ENV['SYMFONY_DEPRECATIONS_SERIALIZE'] = $deprecationsFile;
$process = new PhpProcess($this->getScript($request), null, null);
$process->run();
if (file_exists($deprecationsFile)) {
$deprecations = file_get_contents($deprecationsFile);
unlink($deprecationsFile);
foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) {
if ($deprecation[0]) {
// unsilenced on purpose
trigger_error($deprecation[1], \E_USER_DEPRECATED);
}
else {
@trigger_error($deprecation[1], \E_USER_DEPRECATED);
}
}
}
if (!$process->isSuccessful() || !preg_match('/^O\\:\\d+\\:/', $process->getOutput())) {
throw new RuntimeException(\sprintf('OUTPUT: %s ERROR OUTPUT: %s.', $process->getOutput(), $process->getErrorOutput()));
}
return unserialize($process->getOutput());
}
/**
* Makes a request.
*
* @psalm-param TRequest $request
*
* @return object
*
* @psalm-return TResponse
*/
protected abstract function doRequest(object $request);
/**
* Returns the script to execute when the request must be insulated.
*
* @psalm-param TRequest $request
*
* @param object $request An origin request instance
*
* @return string
*
* @throws LogicException When this abstract class is not implemented
*/
protected function getScript(object $request) {
throw new LogicException('To insulate requests, you need to override the getScript() method.');
}
/**
* Filters the BrowserKit request to the origin one.
*
* @return object
*
* @psalm-return TRequest
*/
protected function filterRequest(Request $request) {
return $request;
}
/**
* Filters the origin response to the BrowserKit one.
*
* @psalm-param TResponse $response
*
* @return Response
*/
protected function filterResponse(object $response) {
return $response;
}
/**
* Creates a crawler.
*
* This method returns null if the DomCrawler component is not available.
*/
protected function createCrawlerFromContent(string $uri, string $content, string $type) : ?Crawler {
if (!class_exists(Crawler::class)) {
return null;
}
$crawler = new Crawler(null, $uri, null, $this->useHtml5Parser);
$crawler->addContent($content, $type);
return $crawler;
}
/**
* Goes back in the browser history.
*/
public function back() : Crawler {
do {
$request = $this->history
->back();
} while (\array_key_exists(serialize($request), $this->redirects));
return $this->requestFromRequest($request, false);
}
/**
* Goes forward in the browser history.
*/
public function forward() : Crawler {
do {
$request = $this->history
->forward();
} while (\array_key_exists(serialize($request), $this->redirects));
return $this->requestFromRequest($request, false);
}
/**
* Reloads the current browser.
*/
public function reload() : Crawler {
return $this->requestFromRequest($this->history
->current(), false);
}
/**
* Follow redirects?
*
* @throws LogicException If request was not a redirect
*/
public function followRedirect() : Crawler {
if (!isset($this->redirect)) {
throw new LogicException('The request was not redirected.');
}
if (-1 !== $this->maxRedirects) {
if ($this->redirectCount > $this->maxRedirects) {
$this->redirectCount = 0;
throw new LogicException(\sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects));
}
}
$request = $this->internalRequest;
if (\in_array($this->internalResponse
->getStatusCode(), [
301,
302,
303,
])) {
$method = 'GET';
$files = [];
$content = null;
}
else {
$method = $request->getMethod();
$files = $request->getFiles();
$content = $request->getContent();
}
if ('GET' === strtoupper($method)) {
// Don't forward parameters for GET request as it should reach the redirection URI
$parameters = [];
}
else {
$parameters = $request->getParameters();
}
$server = $request->getServer();
$server = $this->updateServerFromUri($server, $this->redirect);
$this->isMainRequest = false;
$response = $this->request($method, $this->redirect, $parameters, $files, $server, $content);
$this->isMainRequest = true;
return $response;
}
/**
* @see https://dev.w3.org/html5/spec-preview/the-meta-element.html#attr-meta-http-equiv-refresh
*/
private function getMetaRefreshUrl() : ?string {
$metaRefresh = $this->getCrawler()
->filter('head meta[http-equiv="refresh"]');
foreach ($metaRefresh->extract([
'content',
]) as $content) {
if (preg_match('/^\\s*0\\s*;\\s*URL\\s*=\\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) {
return str_replace("\t\r\n", '', rtrim($m[1]));
}
}
return null;
}
/**
* Restarts the client.
*
* It flushes history and all cookies.
*/
public function restart() : void {
$this->cookieJar
->clear();
$this->history
->clear();
}
/**
* Takes a URI and converts it to absolute if it is not already absolute.
*/
protected function getAbsoluteUri(string $uri) : string {
// already absolute?
if (str_starts_with($uri, 'http://') || str_starts_with($uri, 'https://')) {
return $uri;
}
if (!$this->history
->isEmpty()) {
$currentUri = $this->history
->current()
->getUri();
}
else {
$currentUri = \sprintf('http%s://%s/', isset($this->server['HTTPS']) ? 's' : '', $this->server['HTTP_HOST'] ?? 'localhost');
}
// protocol relative URL
if ('' !== trim($uri, '/') && str_starts_with($uri, '//')) {
return parse_url($currentUri, \PHP_URL_SCHEME) . ':' . $uri;
}
// anchor or query string parameters?
if (!$uri || '#' === $uri[0] || '?' === $uri[0]) {
return preg_replace('/[#?].*?$/', '', $currentUri) . $uri;
}
if ('/' !== $uri[0]) {
$path = parse_url($currentUri, \PHP_URL_PATH);
if (!str_ends_with($path, '/')) {
$path = substr($path, 0, strrpos($path, '/') + 1);
}
$uri = $path . $uri;
}
return preg_replace('#^(.*?//[^/]+)\\/.*$#', '$1', $currentUri) . $uri;
}
/**
* Makes a request from a Request object directly.
*
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
*/
protected function requestFromRequest(Request $request, bool $changeHistory = true) : Crawler {
return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory);
}
private function updateServerFromUri(array $server, string $uri) : array {
$server['HTTP_HOST'] = $this->extractHost($uri);
$scheme = parse_url($uri, \PHP_URL_SCHEME);
$server['HTTPS'] = null === $scheme ? $server['HTTPS'] : 'https' === $scheme;
unset($server['HTTP_IF_NONE_MATCH'], $server['HTTP_IF_MODIFIED_SINCE']);
return $server;
}
private function extractHost(string $uri) : ?string {
$host = parse_url($uri, \PHP_URL_HOST);
if ($port = parse_url($uri, \PHP_URL_PORT)) {
return $host . ':' . $port;
}
return $host;
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overrides |
---|---|---|---|---|
AbstractBrowser::$cookieJar | protected | property | ||
AbstractBrowser::$crawler | protected | property | ||
AbstractBrowser::$followMetaRefresh | protected | property | ||
AbstractBrowser::$followRedirects | protected | property | ||
AbstractBrowser::$history | protected | property | ||
AbstractBrowser::$insulated | protected | property | ||
AbstractBrowser::$internalRequest | protected | property | ||
AbstractBrowser::$internalResponse | protected | property | ||
AbstractBrowser::$isMainRequest | private | property | ||
AbstractBrowser::$maxRedirects | private | property | ||
AbstractBrowser::$redirect | protected | property | ||
AbstractBrowser::$redirectCount | private | property | ||
AbstractBrowser::$redirects | private | property | ||
AbstractBrowser::$request | protected | property | @psalm-var TRequest | |
AbstractBrowser::$response | protected | property | @psalm-var TResponse | |
AbstractBrowser::$server | protected | property | ||
AbstractBrowser::$useHtml5Parser | protected | property | ||
AbstractBrowser::back | public | function | Goes back in the browser history. | |
AbstractBrowser::click | public | function | Clicks on a given link. | |
AbstractBrowser::clickLink | public | function | Clicks the first link (or clickable image) that contains the given text. | |
AbstractBrowser::createCrawlerFromContent | protected | function | Creates a crawler. | |
AbstractBrowser::doRequest | abstract protected | function | Makes a request. | 2 |
AbstractBrowser::doRequestInProcess | protected | function | Makes a request in another process. | |
AbstractBrowser::extractHost | private | function | ||
AbstractBrowser::filterRequest | protected | function | Filters the BrowserKit request to the origin one. | 1 |
AbstractBrowser::filterResponse | protected | function | Filters the origin response to the BrowserKit one. | 1 |
AbstractBrowser::followMetaRefresh | public | function | Sets whether to automatically follow meta refresh redirects or not. | |
AbstractBrowser::followRedirect | public | function | Follow redirects? | |
AbstractBrowser::followRedirects | public | function | Sets whether to automatically follow redirects or not. | |
AbstractBrowser::forward | public | function | Goes forward in the browser history. | |
AbstractBrowser::getAbsoluteUri | protected | function | Takes a URI and converts it to absolute if it is not already absolute. | |
AbstractBrowser::getCookieJar | public | function | Returns the CookieJar instance. | |
AbstractBrowser::getCrawler | public | function | Returns the current Crawler instance. | |
AbstractBrowser::getHistory | public | function | Returns the History instance. | |
AbstractBrowser::getInternalRequest | public | function | Returns the current BrowserKit Request instance. | |
AbstractBrowser::getInternalResponse | public | function | Returns the current BrowserKit Response instance. | |
AbstractBrowser::getMaxRedirects | public | function | Returns the maximum number of redirects that crawler can follow. | |
AbstractBrowser::getMetaRefreshUrl | private | function | ||
AbstractBrowser::getRequest | public | function | Returns the current origin Request instance. | |
AbstractBrowser::getResponse | public | function | Returns the current origin response instance. | |
AbstractBrowser::getScript | protected | function | Returns the script to execute when the request must be insulated. | 1 |
AbstractBrowser::getServerParameter | public | function | Gets single server parameter for specified key. | |
AbstractBrowser::insulate | public | function | Sets the insulated flag. | |
AbstractBrowser::isFollowingRedirects | public | function | Returns whether client automatically follows redirects or not. | |
AbstractBrowser::jsonRequest | public | function | Converts the request parameters into a JSON string and uses it as request content. | |
AbstractBrowser::reload | public | function | Reloads the current browser. | |
AbstractBrowser::request | public | function | Calls a URI. | |
AbstractBrowser::requestFromRequest | protected | function | Makes a request from a Request object directly. | |
AbstractBrowser::restart | public | function | Restarts the client. | |
AbstractBrowser::setMaxRedirects | public | function | Sets the maximum number of redirects that crawler can follow. | |
AbstractBrowser::setServerParameter | public | function | Sets single server parameter. | |
AbstractBrowser::setServerParameters | public | function | Sets server parameters. | |
AbstractBrowser::submit | public | function | Submits a form. | |
AbstractBrowser::submitForm | public | function | Finds the first form that contains a button with the given content and uses it to submit the given form field values. |
|
AbstractBrowser::updateServerFromUri | private | function | ||
AbstractBrowser::useHtml5Parser | public | function | Sets whether parsing should be done using "masterminds/html5". | |
AbstractBrowser::xmlHttpRequest | public | function | ||
AbstractBrowser::__construct | public | function | 2 |