diff --git a/src/Symfony/Component/BrowserKit/CHANGELOG.md b/src/Symfony/Component/BrowserKit/CHANGELOG.md index 185071e6b2..b966974d8d 100644 --- a/src/Symfony/Component/BrowserKit/CHANGELOG.md +++ b/src/Symfony/Component/BrowserKit/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.3.0 ----- + * Added `HttpBrowser`, an implementation of a browser with the HttpClient component * Renamed `Client` to `AbstractBrowser` * Marked `Response` final. * Deprecated `Response::buildHeader()` diff --git a/src/Symfony/Component/BrowserKit/HttpBrowser.php b/src/Symfony/Component/BrowserKit/HttpBrowser.php new file mode 100644 index 0000000000..cb13c738d3 --- /dev/null +++ b/src/Symfony/Component/BrowserKit/HttpBrowser.php @@ -0,0 +1,109 @@ + + * + * 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\HttpClient\HttpClient; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\Multipart\FormDataPart; +use Symfony\Component\Mime\Part\TextPart; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * An implementation of a browser using the HttpClient component + * to make real HTTP requests. + * + * @author Fabien Potencier + * + * @final + */ +class HttpBrowser extends AbstractBrowser +{ + private $client; + + public function __construct(HttpClientInterface $client = null, History $history = null, CookieJar $cookieJar = null) + { + if (!class_exists(HttpClient::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); + } + + $this->client = $client ?? HttpClient::create(); + + parent::__construct([], $history, $cookieJar); + } + + protected function doRequest($request) + { + $headers = $this->getHeaders($request); + $body = ''; + if (null !== $part = $this->getBody($request)) { + $headers = array_merge($headers, $part->getPreparedHeaders()->toArray()); + $body = $part->bodyToIterable(); + } + $response = $this->client->request($request->getMethod(), $request->getUri(), [ + 'headers' => $headers, + 'body' => $body, + 'max_redirects' => 0, + ]); + + return new Response($response->getContent(false), $response->getStatusCode(), $response->getHeaders(false)); + } + + private function getBody(Request $request): ?AbstractPart + { + if (\in_array($request->getMethod(), ['GET', 'HEAD'])) { + return null; + } + + if (!class_exists(AbstractPart::class)) { + throw new \LogicException('You cannot pass non-empty bodies as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + if (null !== $content = $request->getContent()) { + return new TextPart($content, 'utf-8', 'plain', '8bit'); + } + + $fields = $request->getParameters(); + foreach ($request->getFiles() as $name => $file) { + if (!isset($file['tmp_name'])) { + continue; + } + + $fields[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); + } + + return new FormDataPart($fields); + } + + private function getHeaders(Request $request): array + { + $headers = []; + foreach ($request->getServer() as $key => $value) { + $key = strtolower(str_replace('_', '-', $key)); + $contentHeaders = ['content-length' => true, 'content-md5' => true, 'content-type' => true]; + if (0 === strpos($key, 'http-')) { + $headers[substr($key, 5)] = $value; + } elseif (isset($contentHeaders[$key])) { + // CONTENT_* are not prefixed with HTTP_ + $headers[$key] = $value; + } + } + $cookies = []; + foreach ($this->getCookieJar()->allRawValues($request->getUri()) as $name => $value) { + $cookies[] = $name.'='.$value; + } + if ($cookies) { + $headers['cookie'] = implode('; ', $cookies); + } + + return $headers; + } +} diff --git a/src/Symfony/Component/BrowserKit/README.md b/src/Symfony/Component/BrowserKit/README.md index a0083ac5bd..ef6d75b783 100644 --- a/src/Symfony/Component/BrowserKit/README.md +++ b/src/Symfony/Component/BrowserKit/README.md @@ -4,6 +4,9 @@ BrowserKit Component The BrowserKit component simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically. +The component comes with a concrete implementation that uses the HttpClient +component to make real HTTP requests. + Resources --------- diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index 4c0567b5da..89d78c8756 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -21,7 +21,8 @@ }, "require-dev": { "symfony/process": "~3.4|~4.0", - "symfony/css-selector": "~3.4|~4.0" + "symfony/css-selector": "~3.4|~4.0", + "symfony/mime": "^4.3" }, "suggest": { "symfony/process": "" diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index c6630b65bc..9c63f0052e 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -56,7 +56,7 @@ class MockResponse implements ResponseInterface } } - $info['raw_headers'] = $rawHeaders; + $this->info['raw_headers'] = $rawHeaders; } /**