feature #35215 [HttpFoundation] added withers to Cookie class (ns3777k)

This PR was squashed before being merged into the 5.1-dev branch (closes #35215).

Discussion
----------

[HttpFoundation] added withers to Cookie class

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #35212
| License       | MIT
| Doc PR        | -

I was quite descriptive in the issue :-)

The main idea is to get the interface for changing a cookie to avoid every unneeded argument in the constructor.

Current:

```php
$cookie = Cookie::create(
    RegionSwitcher::REGION_COOKIE, $regionSlug, new DateTime('+1 year'), '/',
    $baseDomain, null, false
);
```

This PR:

```php
$cookie = Cookie::create('foo')
            ->withValue('bar')
            ->withExpiresTime(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
            ->withDomain('.myfoodomain.com')
            ->withSecure(true);
```

Every `wither` returns a copy of current cookie with requested setting set. Cookie class remains immutable.

Commits
-------

549afaab17 [HttpFoundation] added withers to Cookie class
This commit is contained in:
Fabien Potencier 2020-01-30 16:43:43 +01:00
commit a2b6085d29
3 changed files with 227 additions and 13 deletions

View File

@ -4,6 +4,9 @@ CHANGELOG
5.1.0
-----
* added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`,
`Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`,
`Cookie::withRaw`, `Cookie::withSameSite`
* Deprecate `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
`__construct()` instead)

View File

@ -99,6 +99,52 @@ class Cookie
throw new \InvalidArgumentException('The cookie name cannot be empty.');
}
$this->name = $name;
$this->value = $value;
$this->domain = $domain;
$this->expire = $this->withExpires($expire)->expire;
$this->path = empty($path) ? '/' : $path;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
}
/**
* Creates a cookie copy with a new value.
*
* @return static
*/
public function withValue(?string $value): self
{
$cookie = clone $this;
$cookie->value = $value;
return $cookie;
}
/**
* Creates a cookie copy with a new domain that the cookie is available to.
*
* @return static
*/
public function withDomain(?string $domain): self
{
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie;
}
/**
* Creates a cookie copy with a new time the cookie expires.
*
* @param int|string|\DateTimeInterface $expire
*
* @return static
*/
public function withExpires($expire = 0): self
{
// convert expiration time to a Unix timestamp
if ($expire instanceof \DateTimeInterface) {
$expire = $expire->format('U');
@ -110,15 +156,75 @@ class Cookie
}
}
$this->name = $name;
$this->value = $value;
$this->domain = $domain;
$this->expire = 0 < $expire ? (int) $expire : 0;
$this->path = empty($path) ? '/' : $path;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$cookie = clone $this;
$cookie->expire = 0 < $expire ? (int) $expire : 0;
return $cookie;
}
/**
* Creates a cookie copy with a new path on the server in which the cookie will be available on.
*
* @return static
*/
public function withPath(string $path): self
{
$cookie = clone $this;
$cookie->path = '' === $path ? '/' : $path;
return $cookie;
}
/**
* Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
*
* @return static
*/
public function withSecure(bool $secure = true): self
{
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie;
}
/**
* Creates a cookie copy that be accessible only through the HTTP protocol.
*
* @return static
*/
public function withHttpOnly(bool $httpOnly = true): self
{
$cookie = clone $this;
$cookie->httpOnly = $httpOnly;
return $cookie;
}
/**
* Creates a cookie copy that uses no url encoding.
*
* @return static
*/
public function withRaw(bool $raw = true): self
{
if ($raw && false !== strpbrk($this->name, self::$reservedCharsList)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name));
}
$cookie = clone $this;
$cookie->raw = $raw;
return $cookie;
}
/**
* Creates a cookie copy with SameSite attribute.
*
* @return static
*/
public function withSameSite(?string $sameSite): self
{
if ('' === $sameSite) {
$sameSite = null;
} elseif (null !== $sameSite) {
@ -129,7 +235,10 @@ class Cookie
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
}
$this->sameSite = $sameSite;
$cookie = clone $this;
$cookie->sameSite = $sameSite;
return $cookie;
}
/**

View File

@ -47,6 +47,15 @@ class CookieTest extends TestCase
Cookie::create($name, null, 0, null, null, null, false, true);
}
/**
* @dataProvider namesWithSpecialCharacters
*/
public function testWithRawThrowsExceptionIfCookieNameContainsSpecialCharacters($name)
{
$this->expectException('InvalidArgumentException');
Cookie::create($name)->withRaw();
}
/**
* @dataProvider namesWithSpecialCharacters
*/
@ -72,6 +81,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', -100);
$this->assertSame(0, $cookie->getExpiresTime());
$cookie = Cookie::create('foo', 'bar')->withExpires(-100);
$this->assertSame(0, $cookie->getExpiresTime());
}
public function testGetValue()
@ -98,6 +111,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', $expire = time() + 3600);
$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
$cookie = Cookie::create('foo')->withExpires($expire = time() + 3600);
$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testGetExpiresTimeIsCastToInt()
@ -105,6 +122,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', 3600.9);
$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer');
$cookie = Cookie::create('foo')->withExpires(3600.6);
$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer');
}
public function testConstructorWithDateTime()
@ -113,6 +134,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', $expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
$cookie = Cookie::create('foo')->withExpires($expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testConstructorWithDateTimeImmutable()
@ -121,6 +146,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', $expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
$cookie = Cookie::create('foo')->withValue('bar')->withExpires($expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testGetExpiresTimeWithStringValue()
@ -130,6 +159,10 @@ class CookieTest extends TestCase
$expire = strtotime($value);
$this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date');
$cookie = Cookie::create('foo')->withValue('bar')->withExpires($value);
$this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date');
}
public function testGetDomain()
@ -137,6 +170,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com');
$this->assertEquals('.myfoodomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid');
$cookie = Cookie::create('foo')->withDomain('.mybardomain.com');
$this->assertEquals('.mybardomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid');
}
public function testIsSecure()
@ -144,6 +181,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', true);
$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS');
$cookie = Cookie::create('foo')->withSecure(true);
$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS');
}
public function testIsHttpOnly()
@ -151,6 +192,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', false, true);
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
$cookie = Cookie::create('foo')->withHttpOnly(true);
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
}
public function testCookieIsNotCleared()
@ -158,6 +203,10 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24);
$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet');
$cookie = Cookie::create('foo')->withExpires(time() + 3600 * 24);
$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet');
}
public function testCookieIsCleared()
@ -166,6 +215,10 @@ class CookieTest extends TestCase
$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired');
$cookie = Cookie::create('foo')->withExpires(time() - 20);
$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired');
$cookie = Cookie::create('foo', 'bar');
$this->assertFalse($cookie->isCleared());
@ -177,21 +230,55 @@ class CookieTest extends TestCase
$cookie = Cookie::create('foo', 'bar', -1);
$this->assertFalse($cookie->isCleared());
$cookie = Cookie::create('foo')->withExpires(-1);
$this->assertFalse($cookie->isCleared());
}
public function testToString()
{
$expected = 'foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly';
$cookie = Cookie::create('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of the cookie');
$cookie = Cookie::create('foo')
->withValue('bar')
->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
->withDomain('.myfoodomain.com')
->withSecure(true)
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of the cookie');
$expected = 'foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly';
$cookie = Cookie::create('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$this->assertEquals($expected, (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$cookie = Cookie::create('foo')
->withValue('bar with white spaces')
->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
->withDomain('.myfoodomain.com')
->withSecure(true)
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$expected = 'foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly';
$cookie = Cookie::create('foo', null, 1, '/admin/', '.myfoodomain.com', false, true, false, null);
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$cookie = Cookie::create('foo')
->withExpires(1)
->withPath('/admin/')
->withDomain('.myfoodomain.com')
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$expected = 'foo=bar; path=/; httponly; samesite=lax';
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals('foo=bar; path=/; httponly; samesite=lax', (string) $cookie);
$this->assertEquals($expected, (string) $cookie);
$cookie = Cookie::create('foo')->withValue('bar');
$this->assertEquals($expected, (string) $cookie);
}
public function testRawCookie()
@ -200,9 +287,21 @@ class CookieTest extends TestCase
$this->assertFalse($cookie->isRaw());
$this->assertEquals('foo=b%20a%20r; path=/', (string) $cookie);
$cookie = Cookie::create('test')->withValue('t e s t')->withHttpOnly(false)->withSameSite(null);
$this->assertFalse($cookie->isRaw());
$this->assertEquals('test=t%20e%20s%20t; path=/', (string) $cookie);
$cookie = Cookie::create('foo', 'b+a+r', 0, '/', null, false, false, true, null);
$this->assertTrue($cookie->isRaw());
$this->assertEquals('foo=b+a+r; path=/', (string) $cookie);
$cookie = Cookie::create('foo')
->withValue('t+e+s+t')
->withHttpOnly(false)
->withRaw(true)
->withSameSite(null);
$this->assertTrue($cookie->isRaw());
$this->assertEquals('foo=t+e+s+t; path=/', (string) $cookie);
}
public function testGetMaxAge()
@ -245,6 +344,9 @@ class CookieTest extends TestCase
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, '');
$this->assertNull($cookie->getSameSite());
$cookie = Cookie::create('foo')->withSameSite('Lax');
$this->assertEquals('lax', $cookie->getSameSite());
}
public function testSetSecureDefault()