class Person {
public private(set) string $fullName;
public function __construct(
public private(set) string $firstName,
public private(set) string $lastName,
public protected(set) int $age,
) {
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
public function update(string $first, string $last): void {
$this->first = $first;
$this->last = $last;
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
}
More flexible than readonly
class Person {
private(set) string $fullName;
public function __construct(
private(set) string $firstName,
private(set) string $lastName,
protected(set) int $age,
) {
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
public function update(string $first, string $last): void {
$this->first = $first;
$this->last = $last;
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
}
Optionally omit public
interface Named {
public string $firstName { get; }
public string $lastName { get; }
public string $nickname { get; set; }
public function fullName(): string;
}
interface Named {
public string $firstName { get; }
public string $lastName { get; }
public string $nickname { get; set; }
public function fullName(): string;
}
class Person implements Named {
public private(set) string $firstName;
public string $lastName;
public string $nickname;
public function fullName(): string {
sprintf('%s %s', $this->firstName, $this->lastName);
}
}
class Person {
public string $name = '' {
get {
return $this->name;
}
set(string $value) {
$this->name = $value;
}
}
}
$p = new Person();
$p->name = "Larry";
print $p->name . PHP_EOL;
set
args are optional
class Person {
public string $name = '' {
get {
return $this->name;
}
set {
$this->name = $value;
}
}
}
// This hasn't changed!
$p = new Person();
$p->name = "Larry";
print $p->name . PHP_EOL;
get
class Person {
public string $name = '' {
get => $this->name;
set {
$this->name = $value;
}
}
}
// This hasn't changed!
$p = new Person();
$p->name = "Larry";
print $p->name . PHP_EOL;
set
class Person {
public string $name = '' {
get => $this->name;
set => $value;
}
}
// This hasn't changed!
$p = new Person();
$p->name = "Larry";
print $p->name . PHP_EOL;
class Person {
private(set) string $fullName;
public function __construct(
public string $firstName,
public string $lastName,
public int $age,
) {
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
}
$p = new Person('James', 'Kirk');
$p->lastName = 'garfield';
print $p->fullName . PHP_EOL;
class Person {
private(set) string $fullName;
public function __construct(
private string $firstName,
private string $lastName,
private int $age,
) {
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
public function getFirstName(): string {
return $this->firstName;
}
public function setFirstName(string $name): void {
$this->firstName = ucfirst($name);
$this->fullName = sprintf('%s %s', $this->firstName, $this->lastName);
}
// x3...
}
$p = new Person('James', 'Kirk');
$p->setLastName('garfield');
print $p->getFullName() . PHP_EOL;
class Person {
private(set) string $fullName {
get => sprintf('%s %s', $this->firstName, $this->lastName);
}
public function __construct(
public string $firstName { set => ucfirst($value); },
public string $lastName { set => ucfirst($value); },
public int $age,
) {}
}
// The API hasn't changed!
$p = new Person('James', 'Kirk');
$p->lastName = 'garfield';
print $p->fullName . PHP_EOL;
class Person {
private(set) string $fullName {
get => sprintf('%s %s', $this->firstName, $this->lastName);
}
public string $firstName {
set => ucfirst($value);
},
public string $lastName {
set => ucfirst($value);
},
public function __construct(
string $first,
string $last,
public int $age,
) {
$this->firstName = $first;
$this->lastName = $last;
}
}
// The API hasn't changed!
$p = new Person('James', 'Kirk');
$p->lastName = 'garfield';
print $p->fullName . PHP_EOL;
A property, in some object-oriented programming languages, is a special sort of class member, intermediate in functionality between a field (or data member) and a method. The syntax for reading and writing of properties is like for fields, but property reads and writes are (usually) translated to 'getter' and 'setter' method calls."Wikipedia: Property
In data hierarchy, a field (data field) is a variable in a record. A record, also known as a data structure, allows logically related data to be identified by a single name."Wikipedia: Field (CS)
Field is a physical aspect of an object
Property is a logical aspect of an object
This changes everything...
Fields are data and implementation details.
Methods are behavior.
You cannot enforce rules on fields if exposed.
readonly
(8.1) and private(set)
(8.4)Programming languages teach you not to want what they cannot provide.—Paul Graham, ANSI Common Lisp
Point(x, y)
DateTime
, Money
, Request
; often immutableCat extends Animal
A Property is a logical aspect or feature of an object
Don't care about the underlying details anymore
Fields are an implementation detail, Properties are an interface
// What timezone is this Date in?
$timestamp->timezone;
// Change the timezone.
$timestamp->timezone = new DateTimeZone('America/Chicago');
// The ID of the object.
$entity->id;
// How big is this list?
$list->size;
// The post this comment is on.
$comment->post;
How are these implemented internally? ¯\_(ツ)_/¯
Pop quiz: What is the second-longest entry in the OED?
"get"
Pop quiz: What is the longest entry in the OED?
"set"
These words mean everything, so they mean nothing
class Person {
private(set) string $fullName {
get => sprintf('%s %s', $this->firstName, $this->lastName);
}
public function __construct(
public string $firstName { set => ucfirst($value); },
public string $lastName { set => ucfirst($value); },
public int $age,
) {}
}
// The API hasn't changed!
$p = new Person('James', 'Kirk');
$p->lastName = 'garfield';
print $p->fullName . PHP_EOL;
Real code I wrote at work last week
interface ResponseMessage {
public ResultStatus $status { get; }
public string $message { get; }
public mixed $result { get; }
}
interface ErrorMessage extends ResponseMessage {
public int $statusCode { get; }
}
enum ResultStatus: string {
case OK = 'OK';
case Error = 'Error';
}
class OKMessage implements ResponseMessage {
// Predefined, immutable
private(set) ResultStatus $status = ResultStatus::OK;
public function __construct(
public readonly string $message = '',
public readonly mixed $result = null,
) {}
}
class NotFoundMessage implements ResponseError {
public ResultStatus $status = ResultStatus::Error;
private(set) int $code = 404;
public function __construct(
public readonly string $message = '',
public readonly mixed $result = null,
) {}
}
class GenericErrorMessage implements ResponseError {
public ResultStatus $status = ResultStatus::Error;
public function __construct(
public readonly string $message = '',
public readonly mixed $result = null,
public readonly int $code = 400,
) {}
}
contact.md
contact.php
contact.latte
All represented by the logical Page
"contact"
(Real code from a side project, MiDy)
interface Page {
public string $title { get; }
public string $summary { get; }
public array $tags { get; }
public bool $hidden { get; }
public bool $routable { get; }
public DateTimeImmutable $publishDate { get; }
public DateTimeImmutable $lastModifiedDate { get; }
public LogicalPath $path { get; }
public PhysicalPath $physicalPath { get; }
public string $folder { get; }
}
interface Page {
public string $title { get; }
public string $summary { get; }
public array $tags { get; }
public bool $hidden { get; }
public bool $routable { get; }
public DateTimeImmutable $publishDate { get; }
public DateTimeImmutable $lastModifiedDate { get; }
public LogicalPath $path { get; }
public PhysicalPath $physicalPath { get; }
public string $folder { get; }
}
class PageRecord implements Page {
public function __construct(
private(set) string $title,
private(set) string $summary,
private(set) DateTimeImmutable $publishDate,
// Implementation detail for this class.
private array $files,
// ...
) {}
}
class PageRecord implements Page {
// This is derived. But how?
public PhysicalPath $physicalPath;
/** @param array<string, File> $files */
public function __construct(
private(set) array $files,
// ...
) {}
}
class PageRecord implements Page {
private File $activeFile {
get => $this->activeFile ??= array_values($this->files)[0];
}
public PhysicalPath $physicalPath {
get => $this->activeFile->physicalPath;
}
/** @param array<string, File> $files */
public function __construct(
private(set) array $files,
// ...
) {}
}
class PageRecord implements Page {
// You need this either way, so why not use it?
private File $activeFile;
private function activeFile(): File {
return $this->activeFile ??= array_values($this->files)[0];
}
public PhysicalPath $physicalPath {
get => $this->activeFile()->physicalPath;
}
/** @param array<string, File> $files */
public function __construct(
private(set) array $files,
// ...
) {}
}
Be conservative in what you send,
be liberal in what you accept.
class PageRecord implements Page {
private(set) LogicalPath $logicalPath {
set(LogicalPath|string $value)
=> is_string($value) ? LogicalPath::create($value) : $value;
}
public function __construct(
LogicalPath|string $logicalPath,
private(set) array $files,
// ...
) {
$this->logicalPath = $logicalPath;
}
}
This is also a form of validation!
class PageRecord implements Page {
private(set) LogicalPath $logicalPath {
set(LogicalPath|string $value)
=> is_string($value) ? LogicalPath::create($value) : $value;
}
public function __construct(
LogicalPath|string $logicalPath,
private(set) array $files,
// ...
) {
$this->logicalPath = $logicalPath;
}
}
class PageRepository {
public function readPage(LogicalPath $path): ?PageRecord {
$result = $this->conn->query(/* ... */, [$path])->queryOne();
return $result ? new PageRecord(...$result) : null;
}
}
enum SortOrder {
case Asc = 'asc';
case Desc = 'desc';
}
class SomeClass {
public SortOrder $order {
set(SortOrder|string $value)
=> is_string($value) ? SortOrder::from($value) : $value;
}
}
Courtesy Dana Luthor on Tuesday
class PageData {
public string $title {
get => $this->values(__PROPERTY__)[0];
}
public bool $hidden {
get => array_all($this->values(__PROPERTY__), static fn($x): bool => (bool)$x);
}
public \DateTimeImmutable $publishDate {
get => \max($this->values(__PROPERTY__));
}
public array $files {
get => array_map(static fn(ParsedFile $f) => $f->toFile(), $this->parsedFiles);
}
// ...
/** @param array<string, ParsedFile> $parsedFiles */
public function __construct(
public LogicalPath $logicalPath,
private array $parsedFiles,
) {}
private function values(string $property): array {
return array_column($this->parsedFiles, $property);
}
}
Requests have lots of properties/attributes/things about them
interface ServerRequestInterface {
public function getProtocolVersion(): string;
public function getBody(): StreamInterface;
public function getMethod(): string;
public function getUri(): Uri;
public function getParsedBody(): null|array|object;
public function getHeaders(): array;
public function getAttributes(): array;
public function hasHeader(string $name): bool;
public function getHeader(string $name): array;
public function getHeaderLine(string $name): string;
public function getAttribute(string $name, mixed $default = null): mixed;
// Lots of with*() methods here, off topic.
}
interface ServerRequestInterface {
public string $protocolVersion { get; }
public StreamInterface $body { get; }
public string $method { get; }
public Uri $uri { get; }
public null|array|object $parsedBody { get; }
public array $headers { get; }
public array $attributes { get; }
public function hasHeader(string $name): bool;
public function getHeader(string $name): array;
public function getHeaderLine(string $name): string;
public function getAttribute(string $name, mixed $default = null): mixed;
// Lots of with*() methods here, off topic.
}
interface ServerRequestInterface {
public string $protocolVersion { get; }
public StreamInterface $body { get; }
public string $method { get; }
public Uri $uri { get; }
public null|array|object $parsedBody { get; }
/** @var array<string, Header> */
public array $headers { get; }
public array $attributes { get; }
// Lots of with*() methods here, off topic.
}
class Header {
public array $raw { get => ... }
public string $line { get => ... }
}
$value = $request->headers['user-agent']?->line;
$hasHeader = isset($request->headers['user-agent']);
interface Request {
public Method $method { get; }
public string $uri { get; }
public ?string $raw { get; }
public array $body { get; }
public RequestHeaders $headers { get; }
public string $path { get; }
public array $query { get; }
/** @var \Tempest\Http\Upload[] $files */
public array $files { get; }
/** @var Cookie[] $cookies */
public array $cookies { get; }
public function has(string $key): bool;
public function hasBody(string $key): bool;
public function hasQuery(string $key): bool;
public function get(string $key, mixed $default = null): mixed;
public function getSessionValue(string $name): mixed;
public function getCookie(string $name): ?Cookie;
}
It used to be anything interesting had to be a method
Now, design your API around concepts,
not implementation
Objects get registered with a service and tell the service about themselves
interface ParseProperties {
public function setProperties(array $properties): void;
public function includePropertiesByDefault(): bool;
public function propertyAttribute(): string;
}
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public readonly array $properties = [];
public function __construct(
public readonly int $a = 0,
) {}
public function setProperties(array $properties): void {
$this->properties = $properties;
}
public function includePropertiesByDefault(): bool {
return true;
}
public function propertyAttribute(): string {
return BasicProperty::class;
}
}
interface ParseProperties {
public function setProperties(array $properties): void;
}
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public array $properties = [];
// Not defined anywhere, you have to just know it.
public bool $includeByDefault = true;
public string $propertyAttribute = BasicProperty::class;
public function __construct(
public readonly int $a = 0,
) {}
public function setProperties(array $properties): void {
$this->properties = $properties;
}
}
interface ParseProperties {
public function setProperties(array $properties): void;
}
#[PropertiesToParse(
includeByDefault: true,
propertyAttribute: BasicProperty::class,
)]
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public array $properties = [];
public function __construct(
public readonly int $a = 0,
) {}
public function setProperties(array $properties): void {
$this->properties = $properties;
}
}
interface ParseProperties {
public function setProperties(array $properties): void;
public bool $includePropertiesByDefault { get; }
public string $propertyAttribute { get; }
}
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public array $properties = [];
private(set) bool $includePropertiesByDefault = true;
public bool $propertyAttribute { get => BasicProperty::class; }
public function __construct(
public readonly int $a = 0,
) {}
public function setProperties(array $properties): void {
$this->properties = $properties;
}
}
interface ParseProperties {
public function setProperties(array $properties): void;
public bool $includePropertiesByDefault { get; }
public string $propertyAttribute { get; }
}
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public array $properties = [];
public bool $includePropertiesByDefault { get => $this->include; }
public bool $propertyAttribute { get => $this->propertiesAs; }
public function __construct(
public readonly int $a = 0,
private readonly $include = true;
private readonly $propertiesAs = BasicProperty::class,
) {}
public function setProperties(array $properties): void {
$this->properties = $properties;
}
}
interface ParseProperties {
public array $properties { set };
public bool $includePropertiesByDefault { get; }
public string $propertyAttribute { get; }
}
#[\Attribute]
class ClassWithProperties implements ParseProperties {
public array $properties {
set {
$this->props = array_filter($value, someFilter(...));
}
}
public bool $includePropertiesByDefault { get => $this->include; }
public bool $propertyAttribute { get => $this->propertiesAs; }
public function __construct(
public readonly int $a = 0,
private readonly $include = true;
private readonly $propertiesAs = BasicProperty::class,
) {}
}
Must you model this way? No
Should you consider modeling this way? Yes
Mentally separate "fields" (stored data) from "properties"
PHP is accelerating...