A Field Guide to PHProperties

Larry Garfield

@Crell@phpc.social

Cover of Exploring PHP 8.0 Cover of Thinking Functionally in PHP

New PHP version! (8.4)

New Toys!

  • Asymmetric visibility
  • Interface properties
  • Property hooks (accessors)
  • And other stuff we're not going to talk about

Asymmetric visibility


          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

Asymmetric visibility


          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 properties


          interface Named {
              public string $firstName { get; }
              public string $lastName { get; }

              public string $nickname { get; set; }

              public function fullName(): string;
          }
        

Interface properties


          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);
              }
          }
        

Property hooks (aka Accessors)


          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;
        

Short-hand 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;
        

Short-hand 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;
        

But... why?


          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;
        

But... why?


          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;
        

No more boilerplate!


          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;
        

But... why?


          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;
        

Hooks are an escape hatch

  • Don't use them
  • OK to make properties public (read or write)
  • Use hooks to preserve BC
  • No more stupid get/set methods!

If you stop here, that's OK!

What if we went all-in on hooks?
What if I told you PHP 8.3 didn't have properties at all?
What even is a property?

Wikipedia answers

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

We've never had properties before now!

This changes everything...

How we learned OOP in my day

Grandpa Simpson yelling at a cloud
  1. Fields are always private/protected
  2. All interaction through methods!

Fields are data and implementation details.
Methods are behavior.

You cannot enforce rules on fields if exposed.

What couldn't fields do?

Enforce a data type
They can since PHP 7.4
Be exposed for read without exposing arbitrary write
readonly (8.1) and private(set) (8.4)
Be counted on to exist
Interface properties in 8.4
Enforce validation rules
But now properties can, with hooks
Enforce business logic
But now properties can, with hooks
Be dynamically computed
But now properties can, with hooks
Anything Fields can do Properties can do better
Join me in rethinking your PHP 5-based assumptions
Programming languages teach you not to want what they cannot provide.
—Paul Graham, ANSI Common Lisp

What is an object?

Product type
Named tuple of typed values. Often exposed: Point(x, y)
Opaque Value objects
Hidden internals; DateTime, Money, Request; often immutable
Entity
Classic OOP academia; often field/get/set. Cat extends Animal
Closures with funny syntax
AKA "service objects"; no data, just functions and dependencies

What is a Property

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

Properties

  • Ask an object about itself
  • Tell an object about itself

For example


          // 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? ¯\_(ツ)_/¯

<aside>

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

</aside>

Properties in action

Backward compatibility shim


          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;
        

Use case: Message value objects

Real code I wrote at work last week

Define a simple value object


          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';
          }
        

Create simple value objects


          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,
            ) {}
          }
        

Use case: Filesystem abstraction

Files

  • contact.md
  • contact.php
  • contact.latte

All represented by the logical Page "contact"

(Real code from a side project, MiDy)

Data object interface


          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; }
          }
        

Data object interface


          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,
                  // ...
              ) {}
          }
        

Lazily computed values


          class PageRecord implements Page {
              // This is derived.  But how?
              public PhysicalPath $physicalPath;

              /** @param array<string, File> $files */
              public function __construct(
                  private(set) array $files,
                  // ...
              ) {}
          }
        

Lazily computed values


          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,
                  // ...
              ) {}
          }
        

Lazily computed values


          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,
                  // ...
              ) {}
          }
        

Postel's law

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!

Database record hydration


          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;
              }
          }
        

Bonus: Enum hydration


          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

Database record derivation


          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);
              }
          }
        

Use case: HTTP modeling

Requests have lots of properties/attributes/things about them

HTTP Request / PSR-7 redux


          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.
          }
        

HTTP Request modern version


          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.
          }
        

HTTP Request, modern version


          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']);
        

HTTP Request, Tempest framework


          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

Use case: Definition interfaces

Objects get registered with a service and tell the service about themselves

Definition interfaces

The traditional way


          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;
              }
          }
        

Definition interfaces

The Laravel way


          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;
              }
          }
        

Definition interfaces

The Symfony/Attributes way


          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;
              }
          }
        

Definition interfaces

The PHP 8.4 way


            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;
              }
          }
        

Definition interfaces

The dynamic way


            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;
              }
          }
        

Definition interfaces

The overdone way?


            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"

Conclusions

  • We have a new mindset available to us
  • Using properties doesn't leak implementation details, get/set methods do!
  • Same effect + less code = happy dev
  • Data modeling is programming. Programming is data modeling.

PHP is accelerating...

An animated PHP elephant, running
* Intro * PHP has new features: hooks, interface props, aviz * Very brief example of each * Easy approach: don't use hooks, write less code, it's an escape hatch * This is OK! If you stop here, you are not doing it wrong. * ... But we can go further. * What is a property? * What is an object? (Philosoraptor) * No one-true OOP * Product types: internals exposed * Compound types: internals hidden (eg, DateTime) * Logical representation (classic OOP academia) * Closures with funny syntax (services, internals hidden) * Historically: All properties private always. Couldn't do better. * "Language teaches you to not want what it doesn't offer." (find source) * Property vs Field * Wikipedia * https://en.wikipedia.org/wiki/Property_(programming) * https://en.wikipedia.org/wiki/Field_(computer_science) * C# * Kotlin * Swift * Others... * What is a property? Depends on the type of object * "Make one thing do many things" (find source) * My def: "A visible aspect of a value." * Method: "An action taken on a value." * Must manipulating an aspect be a method? Depends who you ask... * Things you CAN do with hooks * BC shim * Cached value * Upcasting inside a value object upcast from an array record (eg, DB hydration) *

Larry Garfield

@Crell@phpc.social

All about PHP 8!

https://bit.ly/php80

Cover of Exploring PHP 8.0

Buy my book!

https://bit.ly/fn-php

Cover of Thinking Functionally in PHP

https://www.garfieldtech.com/