Skip to content

Instantly share code, notes, and snippets.

@Crell
Last active January 10, 2023 15:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Crell/2c4c35bbfe3b431b07d09cc730ceb310 to your computer and use it in GitHub Desktop.
Save Crell/2c4c35bbfe3b431b07d09cc730ceb310 to your computer and use it in GitHub Desktop.
Immutable options, PSR-7 example
<?php
// PSR-7 today
class Request implements RequestInterface
{
private UriInterface $uri;
private array $headers = [];
private string $method = 'GET';
private string $version = '1.1';
public function getUri(): UriInterface
{
return $this->uri;
}
public function getMethod(): string
{
return $this->method;
}
public function getVersion(): string
{
return $this->version;
}
public function getHeaders(): array
{
return $this->headers;
}
public function getHeader($name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withMethod(string $method): static
{
$new = clone($this);
$new->method = $method;
return $new;
}
public function withProtocolVersion(string $version): static
{
if (!in_array($version, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException();
}
$new = clone($this);
$new->version = $version;
return $new;
}
public function withUri(UriInterface $uri, bool $preserveHost = false): static
{
$new = clone($this);
$new->uri = $uri;
if ($preserveHost && isset($new->headers['host'])) {
return $new;
}
$new->headers['Host'] = $new->uri->getHost();
return $new;
}
public function withHeader(string $name, string $value): static
{
$new = clone($this);
$new->headers[strtolower($name)] = $value;
return $this;
}
}
$r1 = new Request();
$r2 = $r1->withMethod('POST');
$r3 = $r2->withUri(new Uri('https://php.net/'));
$r4 = $r3->withProtocolVersion('2.0');
$r5 = $r4->withHeader('cache', 'none');
print $r5->getMethod();
print $r5->getUri()->getHost();
print $r5->getProtocolVersion();
print_r($r5->getHeaders());
print $r5->getHeader('cache');
<?php
// PSR-7 with asymmetric visibility and clone-with
class Request implements RequestInterface
{
get:public set:private UriInterface $uri;
get:public set:private $headers = [];
get:public set:private $method = 'GET';
get:public set:private $version = '1.1';
public function __clone(...$args)
{
foreach ($args as $k => $v) {
switch ($k) {
case 'version':
if (!in_array($v, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException($k);
}
$this->version = $v;
break;
case 'uri':
// This will type fail for us if $v isn't a Uri object.
$this->uri = $v;
$this->headers['host'] = $v->host;
break;
case 'method':
case 'headers':
$this->$k = $v;
break;
}
}
}
public function getHeader($name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withMethod(string $method): static
{
return clone($this, method: $method);
}
public function withProtocolVersion(string $version): static
{
if (!in_array($version, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException();
}
return clone($this, version: $version };
}
public function withUri(UriInterface $uri, bool $preserveHost = false): static
{
$new = clone($this, uri: $uri);
if ($preserveHost && isset($new->headers['host'])) {
return $new;
}
$new->headers['Host'] = $new->uri->host;
return $new;
}
public function withHeader(string $name, string $value): static
{
$headers = $this->headers;
$headers[strtolower($name)] = $value;
return clone($this, headers: $headers);
}
}
$r1 = new Request();
$r2 = $r1->withMethod('POST');
$r3 = $r2->withUri(new Uri('https://php.net/'));
$r4 = $r3->withProtocolVersion('2.0');
$r5 = $r4->withHeader('cache', 'none');
print $r5->method;
print $r5->uri->host;
print $r5->version;
print_r($r5->headers);
print $r5->getHeader('cache');
// This errors out correctly, because the properties
// are not publicly settable.
$r6 = clone($r5,
uri: new Uri('https://python.org/'),
headers: [host: 'http://java.com/'],
version: 'the old one',
);
<?php
// PSR-7 with asymmetric visibility and clone-with
class Request implements RequestInterface
{
get:public set:private UriInterface $uri;
get:public set:private $headers = [];
get:public set:private $method = 'GET';
get:public set:private $version = '1.1';
public function getHeader($name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withMethod(string $method): static
{
return clone($this) with {method: $method};
}
public function withProtocolVersion(string $version): static
{
if (!in_array($version, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException();
}
return clone($this) with {version: $version };
}
public function withUri(UriInterface $uri, bool $preserveHost = false): static
{
$new = clone($this) with {uri: $uri};
if ($preserveHost && isset($new->headers['host'])) {
return $new;
}
$new->headers['Host'] = $new->uri->host;
return $new;
}
public function withHeader(string $name, string $value): static
{
$headers = $this->headers;
$headers[strtolower($name)] = $value;
return clone($this) with { headers: $headers };
}
}
$r1 = new Request();
$r2 = $r1->withMethod('POST');
$r3 = $r2->withUri(new Uri('https://php.net/'));
$r4 = $r3->withProtocolVersion('2.0');
$r5 = $r4->withHeader('cache', 'none');
print $r5->method;
print $r5->uri->host;
print $r5->version;
print_r($r5->headers);
print $r5->getHeader('cache');
// This errors out correctly, because the properties
// are not publicly settable.
$r6 = clone($r5) with {
uri: new Uri('https://python.org/'),
headers: [host: 'http://java.com/'],
version: 'the old one',
};
<?php
// PSR-7 with initonly and __clone args
class Request implements RequestInterface
{
public initonly UriInterface $uri;
public initonly array $headers = [];
public initonly string $method = 'GET';
public initonly string $version = '1.1';
public function __clone(...$args)
{
foreach ($args as $k => $v) {
switch ($k) {
case 'version':
if (!in_array($v, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException($k);
}
$this->version = $v;
break;
case 'uri':
// This will type fail for us if $v isn't a Uri object.
$this->uri = $v;
$this->headers['host'] = $v->host;
break;
case 'method':
case 'headers':
$this->$k = $v;
break;
}
}
}
public function getHeader($name): string
{
return $this->headers[strtolower($name)] ?? '';
}
// Still needed because of the $preserveHost = true option.
public function withUri(UriInterface $uri, bool $preserveHost = false): static
{
$args['uri'] = $uri;
// If headers were itself a pseudo-immutable object, this would be even uglier.
if ($preserveHost && isset($this->headers['host'])) {
$headers = $this->headers;
$headers['host'] = $uri->host;
$args['headers'] = $headers;
}
return clone($this, ...$args);
}
public function withHeader(string $name, string $value): static
{
$headers = $this->headers;
$headers[strtolower($name)] = $value;
return clone($this, headers: $headers };
}
}
$r1 = new Request();
$r2 = clone $r1 with {
method: 'POST' };
$r3 = clone($r2, uri: new Uri('https://php.net/'));
$r4 = clone($r3, version: '2.0');
$r5 = $r4->withHeader('cache', 'none');
print $r5->method;
print $r5->uri->host;
print $r5->version;
print_r($r5->headers);
print $r5->getHeader('cache');
// This will now error out.
$r6 = clone($r5,
uri: new Uri('https://python.org/'),
headers: [host: 'http://java.com/'],
version: 'the old one',
);
<?php
// PSR-7 with initonly and clone-with
class Request implements RequestInterface
{
public initonly UriInterface $uri;
public initonly array $headers = [];
public initonly string $method = 'GET';
public initonly string $version = '1.1';
public function getHeader($name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withProtocolVersion(string $version): static
{
if (!in_array($version, ['1.1', '1.0', '2.0'])) {
throw new InvalidArgumentException();
}
return clone($this) with { version: $version };
}
public function withUri(UriInterface $uri, bool $preserveHost = false): static
{
$args['uri'] = $uri;
// If headers were itself a pseudo-immutable object, this would be even uglier.
if ($preserveHost && isset($this->headers['host'])) {
$headers = $this->headers;
$headers['host'] = $uri->host;
$args['headers'] = $headers;
}
return clone($this) with { ...$args };
}
public function withHeader(string $name, string $value): static
{
$headers = $this->headers;
$headers[strtolower($name)] = $value;
return clone($this) with { headers: $headers };
}
}
$r1 = new Request();
$r2 = clone $r1 with { method: 'POST' };
$r3 = $r2->withUri(new Uri('https://php.net/'));
$r4 = $r3->withProtocolVersion('2.0');
$r5 = $r4->withHeader('cache', 'none');
print $r5->method;
print $r5->uri->host;
print $r5->version;
print_r($r5->headers);
print $r5->getHeader('cache');
// This becomes allowed, but shouldn't be.
$r6 = clone($r5) with {
uri: new Uri('https://python.org/'),
headers: [host: 'http://java.com/'],
version: 'the old one',
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment