Browse Source

Merge pull request #8822 from cakephp/psr7-client-response

PSR7 Http\Client response
José Lorenzo Rodríguez 10 years ago
parent
commit
fc6cddc04b

+ 1 - 33
src/Http/Client/Message.php

@@ -128,13 +128,6 @@ class Message
     const METHOD_HEAD = 'HEAD';
 
     /**
-     * The array of headers in the response.
-     *
-     * @var array
-     */
-    protected $_headers = [];
-
-    /**
      * The array of cookies in the response.
      *
      * @var array
@@ -142,20 +135,6 @@ class Message
     protected $_cookies = [];
 
     /**
-     * Normalize header names to Camel-Case form.
-     *
-     * @param string $name The header name to normalize.
-     * @return string Normalized header name.
-     */
-    protected function _normalizeHeader($name)
-    {
-        $parts = explode('-', trim($name));
-        $parts = array_map('strtolower', $parts);
-        $parts = array_map('ucfirst', $parts);
-        return implode('-', $parts);
-    }
-
-    /**
      * Get all headers
      *
      * @return array
@@ -163,7 +142,7 @@ class Message
      */
     public function headers()
     {
-        return $this->_headers;
+        return $this->headers;
     }
 
     /**
@@ -177,17 +156,6 @@ class Message
     }
 
     /**
-     * Get the HTTP version used.
-     *
-     * @return string
-     * @deprecated 3.3.0 Use getProtocolVersion()
-     */
-    public function version()
-    {
-        return $this->protocol;
-    }
-
-    /**
      * Get/set the body for the message.
      *
      * @param string|null $body The body for the request. Leave null for get

+ 181 - 49
src/Http/Client/Response.php

@@ -13,7 +13,10 @@
  */
 namespace Cake\Http\Client;
 
+use Psr\Http\Message\ResponseInterface;
 use RuntimeException;
+use Zend\Diactoros\MessageTrait;
+use Zend\Diactoros\Stream;
 
 /**
  * Implements methods for HTTP responses.
@@ -27,14 +30,14 @@ use RuntimeException;
  * when the response is parsed.
  *
  * ```
- * $val = $response->header('content-type');
+ * $val = $response->getHeaderLine('content-type');
  * ```
  *
  * Will read the Content-Type header. You can get all set
  * headers using:
  *
  * ```
- * $response->header();
+ * $response->getHeaders();
  * ```
  *
  * You can also get at the headers using object access. When getting
@@ -47,13 +50,14 @@ use RuntimeException;
  *
  * ### Get the response body
  *
- * You can access the response body using:
+ * You can access the response body stream using:
  *
  * ```
- * $content = $response->body();
+ * $content = $response->getBody();
  * ```
  *
- * You can also use object access:
+ * You can also use object access to get the string version
+ * of the response body:
  *
  * ```
  * $content = $response->body;
@@ -78,7 +82,7 @@ use RuntimeException;
  * You can access the response status code using:
  *
  * ```
- * $content = $response->statusCode();
+ * $content = $response->getStatusCode();
  * ```
  *
  * You can also use object access:
@@ -87,28 +91,23 @@ use RuntimeException;
  * $content = $response->code;
  * ```
  */
-class Response extends Message
+class Response extends Message implements ResponseInterface
 {
-    /**
-     * This is temporary until the response is made PSR7 compliant as well.
-     *
-     * @var string
-     */
-    protected $protocol = '1.1';
+    use MessageTrait;
 
     /**
      * The status code of the response.
      *
      * @var int
      */
-    protected $_code;
+    protected $code;
 
     /**
-     * The response body
+     * The reason phrase for the status code
      *
      * @var string
      */
-    protected $_body;
+    protected $reasonPhrase;
 
     /**
      * Cached decoded XML data.
@@ -131,11 +130,11 @@ class Response extends Message
      */
     protected $_exposedProperties = [
         'cookies' => '_cookies',
-        'headers' => '_headers',
-        'body' => '_body',
-        'code' => '_code',
+        'body' => '_getBody',
+        'code' => 'code',
         'json' => '_getJson',
-        'xml' => '_getXml'
+        'xml' => '_getXml',
+        'headers' => '_getHeaders',
     ];
 
     /**
@@ -147,10 +146,12 @@ class Response extends Message
     public function __construct($headers = [], $body = '')
     {
         $this->_parseHeaders($headers);
-        if ($this->header('Content-Encoding') === 'gzip') {
+        if ($this->getHeaderLine('Content-Encoding') === 'gzip') {
             $body = $this->_decodeGzipBody($body);
         }
-        $this->_body = $body;
+        $stream = new Stream('php://memory', 'wb+');
+        $stream->write($body);
+        $this->stream = $stream;
     }
 
     /**
@@ -182,7 +183,7 @@ class Response extends Message
     /**
      * Parses headers if necessary.
      *
-     * - Decodes the status code.
+     * - Decodes the status code and reasonphrase.
      * - Parses and normalizes header names + values.
      *
      * @param array $headers Headers to parse.
@@ -192,22 +193,26 @@ class Response extends Message
     {
         foreach ($headers as $key => $value) {
             if (substr($value, 0, 5) === 'HTTP/') {
-                preg_match('/HTTP\/([\d.]+) ([0-9]+)/i', $value, $matches);
+                preg_match('/HTTP\/([\d.]+) ([0-9]+)(.*)/i', $value, $matches);
                 $this->protocol = $matches[1];
-                $this->_code = $matches[2];
+                $this->code = $matches[2];
+                $this->reasonPhrase = trim($matches[3]);
                 continue;
             }
             list($name, $value) = explode(':', $value, 2);
             $value = trim($value);
-            $name = $this->_normalizeHeader($name);
-            if ($name === 'Set-Cookie') {
+            $name = trim($name);
+
+            $normalized = strtolower($name);
+            if ($normalized === 'set-cookie') {
                 $this->_parseCookie($value);
             }
-            if (isset($this->_headers[$name])) {
-                $this->_headers[$name] = (array)$this->_headers[$name];
-                $this->_headers[$name][] = $value;
+
+            if (isset($this->headers[$name])) {
+                $this->headers[$name][] = $value;
             } else {
-                $this->_headers[$name] = $value;
+                $this->headers[$name] = (array)$value;
+                $this->headerNames[$normalized] = $name;
             }
         }
     }
@@ -264,7 +269,7 @@ class Response extends Message
             static::STATUS_CREATED,
             static::STATUS_ACCEPTED
         ];
-        return in_array($this->_code, $codes);
+        return in_array($this->code, $codes);
     }
 
     /**
@@ -281,8 +286,8 @@ class Response extends Message
             static::STATUS_TEMPORARY_REDIRECT,
         ];
         return (
-            in_array($this->_code, $codes) &&
-            $this->header('Location')
+            in_array($this->code, $codes) &&
+            $this->getHeaderLine('Location')
         );
     }
 
@@ -290,20 +295,67 @@ class Response extends Message
      * Get the status code from the response
      *
      * @return int
+     * @deprecated 3.3.0 Use getStatusCode() instead.
      */
     public function statusCode()
     {
-        return $this->_code;
+        return $this->code;
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @return int The status code.
+     */
+    public function getStatusCode()
+    {
+        return $this->code;
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @param int $code The status code to set.
+     * @param string $reasonPhrase The status reason phrase.
+     * @return self A copy of the current object with an updated status code.
+     */
+    public function withStatus($code, $reasonPhrase = '')
+    {
+        $new = clone $this;
+        $new->code = $code;
+        $new->reasonPhrase = $reasonPhrase;
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @return string The current reason phrase.
+     */
+    public function getReasonPhrase()
+    {
+        return $this->reasonPhrase;
     }
 
     /**
      * Get the encoding if it was set.
      *
      * @return string|null
+     * @deprecated 3.3.0 Use getEncoding() instead.
      */
     public function encoding()
     {
-        $content = $this->header('content-type');
+        return $this->getEncoding();
+    }
+
+    /**
+     * Get the encoding if it was set.
+     *
+     * @return string|null
+     */
+    public function getEncoding()
+    {
+        $content = $this->getHeaderLine('content-type');
         if (!$content) {
             return null;
         }
@@ -323,43 +375,95 @@ class Response extends Message
      *   will be returned when getting all headers or when getting
      *   a header that had multiple values set. Otherwise a string
      *   will be returned.
+     * @deprecated 3.3.0 Use getHeader() and getHeaderLine() instead.
      */
     public function header($name = null)
     {
         if ($name === null) {
-            return $this->_headers;
+            return $this->_getHeaders();
         }
-        $name = $this->_normalizeHeader($name);
-        if (!isset($this->_headers[$name])) {
-            return null;
+        $header = $this->getHeader($name);
+        if (count($header) === 1) {
+            return $header[0];
         }
-        return $this->_headers[$name];
+        return $header;
     }
 
     /**
      * Read single/multiple cookie values out.
      *
+     * *Note* This method will only provide access to cookies that
+     * were added as part of the constructor. If cookies are added post
+     * construction they will not be accessible via this method.
+     *
      * @param string|null $name The name of the cookie you want. Leave
      *   null to get all cookies.
      * @param bool $all Get all parts of the cookie. When false only
      *   the value will be returned.
      * @return mixed
+     * @deprecated 3.3.0 Use getCookie(), getCookieData() or getCookies() instead.
      */
     public function cookie($name = null, $all = false)
     {
         if ($name === null) {
-            return $this->_cookies;
+            return $this->getCookies();
+        }
+        if ($all) {
+            return $this->getCookieData($name);
         }
+        return $this->getCookie($name);
+    }
+
+    /**
+     * Get the all cookie data.
+     *
+     * @return array The cookie data
+     */
+    public function getCookies()
+    {
+        return $this->_cookies;
+    }
+
+    /**
+     * Get the value of a single cookie.
+     *
+     * @param string $name The name of the cookie value.
+     * @return string|null Either the cookie's value or null when the cookie is undefined.
+     */
+    public function getCookie($name)
+    {
         if (!isset($this->_cookies[$name])) {
             return null;
         }
-        if ($all) {
-            return $this->_cookies[$name];
-        }
         return $this->_cookies[$name]['value'];
     }
 
     /**
+     * Get the full data for a single cookie.
+     *
+     * @param string $name The name of the cookie value.
+     * @return array|null Either the cookie's data or null when the cookie is undefined.
+     */
+    public function getCookieData($name)
+    {
+        if (!isset($this->_cookies[$name])) {
+            return null;
+        }
+        return $this->_cookies[$name];
+    }
+
+    /**
+     * Get the HTTP version used.
+     *
+     * @return string
+     * @deprecated 3.3.0 Use getProtocolVersion()
+     */
+    public function version()
+    {
+        return $this->protocol;
+    }
+
+    /**
      * Get the response body.
      *
      * By passing in a $parser callable, you can get the decoded
@@ -377,10 +481,12 @@ class Response extends Message
      */
     public function body($parser = null)
     {
+        $stream = $this->stream;
+        $stream->rewind();
         if ($parser) {
-            return $parser($this->_body);
+            return $parser($stream->getContents());
         }
-        return $this->_body;
+        return $stream->getContents();
     }
 
     /**
@@ -393,7 +499,7 @@ class Response extends Message
         if (!empty($this->_json)) {
             return $this->_json;
         }
-        return $this->_json = json_decode($this->_body, true);
+        return $this->_json = json_decode($this->_getBody(), true);
     }
 
     /**
@@ -407,7 +513,7 @@ class Response extends Message
             return $this->_xml;
         }
         libxml_use_internal_errors();
-        $data = simplexml_load_string($this->_body);
+        $data = simplexml_load_string($this->_getBody());
         if ($data) {
             $this->_xml = $data;
             return $this->_xml;
@@ -416,6 +522,32 @@ class Response extends Message
     }
 
     /**
+     * Provides magic __get() support.
+     *
+     * @return array
+     */
+    protected function _getHeaders()
+    {
+        $out = [];
+        foreach ($this->headers as $key => $values) {
+            $out[$key] = implode(',', $values);
+        }
+        return $out;
+    }
+
+    /**
+     * Provides magic __get() support.
+     *
+     * @return array
+     */
+    protected function _getBody()
+    {
+        $this->stream->rewind();
+        return $this->stream->getContents();
+    }
+
+
+    /**
      * Read values as properties.
      *
      * @param string $name Property name.

+ 84 - 1
tests/TestCase/Network/Http/ResponseTest.php

@@ -21,6 +21,33 @@ use Cake\TestSuite\TestCase;
  */
 class ResponseTest extends TestCase
 {
+    /**
+     * Test parsing headers and reading with PSR7 methods.
+     *
+     * @return void
+     */
+    public function testHeaderParsingPsr7()
+    {
+        $headers = [
+            'HTTP/1.0 200 OK',
+            'Content-Type : text/html;charset="UTF-8"',
+            'date: Tue, 25 Dec 2012 04:43:47 GMT',
+        ];
+        $response = new Response($headers, 'winner!');
+
+        $this->assertEquals('1.0', $response->getProtocolVersion());
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('OK', $response->getReasonPhrase());
+        $this->assertEquals(
+            'text/html;charset="UTF-8"',
+            $response->getHeaderLine('content-type')
+        );
+        $this->assertEquals(
+            'Tue, 25 Dec 2012 04:43:47 GMT',
+            $response->getHeaderLine('Date')
+        );
+        $this->assertEquals('winner!', '' . $response->getBody());
+    }
 
     /**
      * Test parsing headers and capturing content
@@ -36,8 +63,8 @@ class ResponseTest extends TestCase
         ];
         $response = new Response($headers, 'ok');
 
-        $this->assertEquals('1.0', $response->version());
         $this->assertEquals(200, $response->statusCode());
+        $this->assertEquals('1.0', $response->version());
         $this->assertEquals(
             'text/html;charset="UTF-8"',
             $response->header('content-type')
@@ -120,6 +147,22 @@ class ResponseTest extends TestCase
     }
 
     /**
+     * Test accessor for json when set with PSR7 methods.
+     *
+     * @return void
+     */
+    public function testBodyJsonPsr7()
+    {
+        $data = [
+            'property' => 'value'
+        ];
+        $encoded = json_encode($data);
+        $response = new Response([], '');
+        $response->getBody()->write($encoded);
+        $this->assertEquals($data, $response->json);
+    }
+
+    /**
      * Test accessor for xml
      *
      * @return void
@@ -252,6 +295,42 @@ XML;
     }
 
     /**
+     * Test accessing cookies through the PSR7-like methods
+     *
+     * @return void
+     */
+    public function testGetCookies()
+    {
+        $headers = [
+            'HTTP/1.0 200 Ok',
+            'Set-Cookie: test=value',
+            'Set-Cookie: session=123abc',
+            'Set-Cookie: expiring=soon; Expires=Wed, 09-Jun-2021 10:18:14 GMT; Path=/; HttpOnly; Secure;',
+        ];
+        $response = new Response($headers, '');
+
+        $this->assertNull($response->getCookie('undef'));
+        $this->assertEquals('value', $response->getCookie('test'));
+        $this->assertEquals('soon', $response->getCookie('expiring'));
+
+        $result = $response->getCookieData('expiring');
+        $this->assertEquals('soon', $result['value']);
+        $this->assertTrue($result['httponly']);
+        $this->assertTrue($result['secure']);
+        $this->assertEquals(
+            'Wed, 09-Jun-2021 10:18:14 GMT',
+            $result['expires']
+        );
+        $this->assertEquals('/', $result['path']);
+
+        $result = $response->getCookies();
+        $this->assertCount(3, $result);
+        $this->assertArrayHasKey('test', $result);
+        $this->assertArrayHasKey('session', $result);
+        $this->assertArrayHasKey('expiring', $result);
+    }
+
+    /**
      * Test statusCode()
      *
      * @return void
@@ -264,6 +343,7 @@ XML;
         ];
         $response = new Response($headers, '');
         $this->assertEquals(404, $response->statusCode());
+        $this->assertEquals(404, $response->getStatusCode());
 
         $this->assertEquals(404, $response->code);
         $this->assertTrue(isset($response->code));
@@ -287,6 +367,7 @@ XML;
             'Content-Type: text/html'
         ];
         $response = new Response($headers, '');
+        $this->assertNull($response->getEncoding());
         $this->assertNull($response->encoding());
 
         $headers = [
@@ -294,6 +375,7 @@ XML;
             'Content-Type: text/html; charset="UTF-8"'
         ];
         $response = new Response($headers, '');
+        $this->assertEquals('UTF-8', $response->getEncoding());
         $this->assertEquals('UTF-8', $response->encoding());
 
         $headers = [
@@ -301,6 +383,7 @@ XML;
             "Content-Type: text/html; charset='ISO-8859-1'"
         ];
         $response = new Response($headers, '');
+        $this->assertEquals('ISO-8859-1', $response->getEncoding());
         $this->assertEquals('ISO-8859-1', $response->encoding());
     }
 

+ 1 - 1
tests/TestCase/ORM/QueryRegressionTest.php

@@ -129,10 +129,10 @@ class QueryRegressionTest extends TestCase
             ->order(['Articles.id' => 'ASC'])
             ->toArray();
 
+        $this->assertCount(5, $results);
         $this->assertEquals(1, $results[0]->articles_tag->foo->id);
         $this->assertEquals(1, $results[0]->author->favorite_tag->id);
         $this->assertEquals(2, $results[1]->articles_tag->foo->id);
-        $this->assertEquals(1, $results[0]->author->favorite_tag->id);
         $this->assertEquals(1, $results[2]->articles_tag->foo->id);
         $this->assertEquals(3, $results[2]->author->favorite_tag->id);
         $this->assertEquals(3, $results[3]->articles_tag->foo->id);