Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5e7319c
initial commit
withinboredom Aug 1, 2024
3be33e9
change format to ptxt
withinboredom Aug 1, 2024
bd4cf96
apply some off-line feedback
withinboredom Aug 1, 2024
c69bff3
apply some off-line feedback
withinboredom Aug 1, 2024
49be882
Update drafts/records.md
withinboredom Aug 1, 2024
090855f
Automated changes from GitHub Actions
github-actions[bot] Aug 1, 2024
c91e18c
be more assertive
withinboredom Aug 1, 2024
7b1e32b
fix lint
withinboredom Aug 1, 2024
be7ae31
address automated feedback
withinboredom Aug 1, 2024
e9a1cc1
clarify
withinboredom Aug 1, 2024
97057b4
serialization and deserialization
withinboredom Aug 1, 2024
fc83031
clarity
withinboredom Aug 1, 2024
62c8edd
fix serialization and deserialization
withinboredom Aug 1, 2024
a490ba0
apply some feedback
withinboredom Aug 1, 2024
eb4f020
experiment with instructions
withinboredom Aug 1, 2024
9089f44
make parsable instructions since yaml parser is broke
withinboredom Aug 1, 2024
6b32127
add more details to example
withinboredom Aug 1, 2024
f7c3a05
address feedback and add more details
withinboredom Aug 2, 2024
a2718a6
address feedback
withinboredom Aug 2, 2024
714a445
grammar
withinboredom Aug 2, 2024
12d702d
clarity
withinboredom Aug 2, 2024
57109f5
refactor reflection and make it make more sense
withinboredom Aug 2, 2024
1b91037
address feedback
withinboredom Aug 2, 2024
91d287e
remove trailing space
withinboredom Aug 2, 2024
ce74737
add information about non-trivial values
withinboredom Aug 2, 2024
30d19d9
make comparisons undefined
withinboredom Aug 2, 2024
c1d0a8a
add function autoloading
withinboredom Aug 15, 2024
a320634
reword things
withinboredom Aug 15, 2024
908d482
update example
withinboredom Aug 15, 2024
5134c01
clarify more
withinboredom Aug 15, 2024
72f3856
further clarification
withinboredom Aug 15, 2024
1b50121
define function_exists
withinboredom Aug 15, 2024
1020c04
fix a typo
withinboredom Aug 16, 2024
5b3a6c5
add mismatched-arguments
withinboredom Aug 18, 2024
df2e506
always pull
withinboredom Aug 18, 2024
eb437b9
remove whitespace
withinboredom Aug 18, 2024
a6ff51d
always pull
withinboredom Aug 18, 2024
16e6967
apparently, spl_autoload does need some changes
withinboredom Aug 18, 2024
c6cbaf0
updated rfc
withinboredom Nov 16, 2024
6cd68e6
updated rfc
withinboredom Nov 16, 2024
fdb8dd6
Automated changes from GitHub Actions
github-actions[bot] Nov 16, 2024
f14b093
updated author
withinboredom Nov 16, 2024
c7155c3
added interface documentation
withinboredom Nov 16, 2024
02ed26f
update records
withinboredom Nov 16, 2024
be0024a
move to under discussion
withinboredom Nov 16, 2024
c2e5eef
new functions
withinboredom Nov 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ all: $(PUBLISHED) .git/hooks/pre-commit

drafts/template.md: template.ptxt
@echo "Creating draft from template"
src/convert-to-md.sh template.txt drafts/template.md
src/convert-to-md.sh template.ptxt drafts/template.md

published/%.ptxt: drafts/%.md
@echo "Converting $< to $@"
Expand Down
Empty file removed drafts/.gitkeep
Empty file.
360 changes: 360 additions & 0 deletions drafts/records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
# PHP RFC: Records

- Version: 0.9
- Date: 2024-07-19
- Author: Robert Landers, landers.robert@gmail.com
- Status: Draft (or Under Discussion or Accepted or Declined)
Comment thread
withinboredom marked this conversation as resolved.
Outdated
- First Published at: <http://wiki.php.net/rfc/records>

## Introduction

This RFC proposed the introduction of `record` objects, which are immutable classes
Comment thread
withinboredom marked this conversation as resolved.
Outdated
with [value semantics](https://en.wikipedia.org/wiki/Value_semantics).

### Value objects

Value objects are immutable objects that represent a value. They are used to store values with a different meaning than
Comment thread
withinboredom marked this conversation as resolved.
Outdated
their technical value.
For example, a `Point` object with `x` and `y` properties can represent a point in a 2D space,
and an `ExpirationDate` can represent a date when something expires.
This prevents developers from accidentally using the wrong value in the wrong context.

Consider this example:
Comment thread
withinboredom marked this conversation as resolved.
Outdated

```php
function updateUserRole(int $userId, Role $role): void {
// ...
}

$user = getUser(/*...*/)
$uid = $user->id;
// ...
$uid = 5; // somehow accidentally sets uid to an unrelated integer
// ...
updateUserRole($uid, Role::ADMIN()); // accidental passing of
Comment thread
withinboredom marked this conversation as resolved.
Outdated
```

In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code.

#### The solution

Like arrays, strings, and other values, `record` objects are strongly equal to each other if they contain the same
values.

Let's take a look, using the previous example:
Comment thread
withinboredom marked this conversation as resolved.
Outdated

```php
record UserId(int $id);

function updateUserRole(UserId $userId, Role $role): void {
// ...
}

$user = getUser(/*...*/)
$uid = $user->id; // $uid is a UserId object
// ...
$uid = 5;
// ...
updateUserRole($uid, Role::ADMIN()); // This will throw an error
```

## Proposal

This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will
Comment thread
withinboredom marked this conversation as resolved.
Outdated
allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying
properties and equality checks using a function-like instantiation syntax.
Records can implement interfaces and use traits but cannot extend other records or classes;
composition is allowed, however.

#### Syntax and semantics

##### Definition

A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more
typed parameters that become public, immutable, properties.
Comment thread
withinboredom marked this conversation as resolved.
Outdated
They may optionally implement an interface using the `implements` keyword.
A `record` body is optional.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

A `record` may contain a constructor with zero arguments to perform further initialization, if required.
Comment thread
withinboredom marked this conversation as resolved.
Outdated
If it does not have a constructor, an implicit, empty contstructor is provided.

A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record`
rules).
Regular properties may also be defined, but they are immutable by default and are no different from `const`.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

Static properties and methods are forbidden in a `record` (this includes
`const`, a regular property may be used instead). Attempting to define
static properties, methods, constants results in a compilation error.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

``` php
Comment thread
withinboredom marked this conversation as resolved.
namespace Paint;

record Pigment(int $red, int $yellow, int $blue) {
public function mix(Pigment $other, float $amount): Pigment {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use a more semantically meaningful name than $amount for those of us not well verse in color theory?

Comment thread
withinboredom marked this conversation as resolved.
return $this->with(
red: $this->red * (1 - $amount) + $other->red * $amount,
yellow: $this->yellow * (1 - $amount) + $other->yellow * $amount,
blue: $this->blue * (1 - $amount) + $other->blue * $amount
);
}
}

record StockPaint(Pigment $color, float $volume);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshedding, but I don't get what StockPaint intends to imply. I thought it might be "the amount of paint you have in stock" but then the code doesn't seem to support that understanding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like when you go to the store and buy some paint. You choose a color and then they mix it for you. The colors they put into the mixer would be the "stock".

Comment thread
withinboredom marked this conversation as resolved.

record PaintBucket(StockPaint ...$constituents) {
public function mixIn(StockPaint $paint): PaintBucket {
return $this->with(...$this->constituents, $paint);
}

public function color(): Pigment {
return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0));
}
}
```

##### Usage

A `record` may be used as a `readonly class`,
Comment thread
withinboredom marked this conversation as resolved.
Outdated
Comment thread
withinboredom marked this conversation as resolved.
Outdated
as the behavior of it is very similar with no key differences to assist in migration from `readonly class`.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

##### Optional parameters and default values

A `record` can also be defined with optional parameters that are set if left out during instantiation.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

``` php
record Rectangle(int $x, int $y = 10);
var_dump(Rectangle(10)); // output a record with x: 10 and y: 10
```

##### Auto-generated `with` method

To enhance the usability of records, the RFC proposes automatically generating a `with` method for each record.
This method allows for partial updates of properties, creating a new instance of the record with the specified
properties updated.

The auto-generated `with` method accepts only named arguments defined in the constructor.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a with() accept an array of those properties?

$point2 = $point1->with([ "x" => 5 ]);

or:

$point2 = $point1->with(...[ "x" => 5 ]);

If not, why not?

The issue is that PHP has not a type I would call a "parameter set" or more succinctly, "args" so the only way to pass them around from function to function is via arrays.

I could also envision records defined to be args for other records, functions and/or classes e.g.:

record PointArgs(int $x, int $y)
record Point(int $x, int $y) {
  // Point stuff here
}
function DoSomething(Point $p, PointArgs $pa) {
   $p2 = $p->with($pa) 
}
$pa = PointArgs(5,10);
$p = Point($pa);
DoSomething($p, PointArgs(3,9));

Along those lines, is certainly would be nice if I could use a shorthand like this:

DoSomething($p, {3,9});

And/or:

DoSomething($p, {x:3,y:9});

Something to ponder?

Comment thread
withinboredom marked this conversation as resolved.
Outdated
No other property names can be used, and it returns a new record object with the given values.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

``` php
$point1 = Point(3, 4);
$point2 = $point1->with(x: 5);
$point3 = $point1->with(null, 10); // must use named arguments
Comment thread
withinboredom marked this conversation as resolved.
Outdated

echo $point1->x; // Outputs: 3
echo $point2->x; // Outputs: 5
```

A developer may define their own `with` method if they so choose,
Comment thread
withinboredom marked this conversation as resolved.
Outdated
and reference the generated `with` method using `parent::with()`.
Comment thread
withinboredom marked this conversation as resolved.
This allows a developer to define policies or constraints on how data is updated.

``` php
record Planet(string $name, int $population) {
public function with(int $population) {
return parent::with(population: $population);
}
Comment thread
withinboredom marked this conversation as resolved.
Outdated
}
$pluto = Planet("Pluto", 0);
// we made it!
$pluto = $pluto->with(population: 1);
// and then we changed the name
$mickey = $pluto->with(name: "Mickey"); // no named argument for population error
Comment thread
withinboredom marked this conversation as resolved.
Outdated
```

##### Constructors

Optionally, they may also define a constructor to provide validation or other initialization logic:
Comment thread
withinboredom marked this conversation as resolved.
Outdated

```php
record User(string $name, string $email) {
public string $id;

public function __construct() {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email address");
}

$this->id = hash('sha256', $email);
$this->name = ucwords($name);
}
}
```

During construction, a `record` is fully mutable.
This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object.

#### Performance considerations

To ensure that records are both performant and memory-efficient,
the RFC proposes leveraging PHP's copy-on-write (COW) semantics (similar to arrays) and interning values.
Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they are no
longer needed.

``` php
$point1 = Point(3, 4);
$point2 = $point1; // No data duplication, $point2 references the same data as $point1
$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1

$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data
Comment thread
withinboredom marked this conversation as resolved.
Outdated
```

##### Cloning and with()

Calling `clone` on a `record` results in the exact same record object being returned. As it is a "value" object, it
Comment thread
withinboredom marked this conversation as resolved.
Outdated
represents a value and is the same thing as saying `clone 3`—you expect to get back a `3`.

`with` may be called with no arguments, and it is the same behavior as `clone`.
This is an important consideration because a developer may call `$new = $record->with(...$array)` and we don’t want to
crash.
If a developer wants to crash, they can do by `assert($new !== $record)`.

#### Equality

A `record` is always strongly equal (`===`) to another record with the same value in the properties,
much like an `array` is strongly equal to another array containing the same elements.
For all intents, `$recordA === $recordB` is the same as `$recordA == $recordB`.

Comparison operations will behave exactly like they do for classes.

### Reflection

Records in PHP will be fully supported by the reflection API,
providing access to their properties and methods just like regular classes.
However, immutability and special instantiation rules will be enforced.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

#### ReflectionClass support

`ReflectionClass` can be used to inspect records, their properties, and methods. Any attempt to modify record properties
Comment thread
withinboredom marked this conversation as resolved.
Outdated
via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via
`ReflectionClass` will cause a `ReflectionException` to be thrown.

``` php
$point = Point(3, 4);
$reflection = new \ReflectionClass($point);

foreach ($reflection->getProperties() as $property) {
echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL;
}
```

#### Immutability enforcement

Attempts to modify record properties via reflection will throw an exception.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

``` php
try {
$property = $reflection->getProperty('x');
$property->setValue($point, 10); // This will throw an exception
} catch (\ReflectionException $e) {
echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property"
}
```

#### ReflectionFunction for implicit constructor
Comment thread
withinboredom marked this conversation as resolved.
Outdated

Using `ReflectionFunction` on a record will reflect the implicit constructor.

``` php
$constructor = new \ReflectionFunction('Geometry\Point');
echo 'Constructor Parameters: ';
foreach ($constructor->getParameters() as $param) {
echo $param->getName() . ' ';
}
```

#### New functions and methods
Comment thread
withinboredom marked this conversation as resolved.
Outdated

- Calling `is_object($record)` will return `true`.
- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise
- Calling `get_class($record)` will return the record name
Comment thread
withinboredom marked this conversation as resolved.
Outdated

#### var_dump

Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

record(Point)#1 (2) {
["x"]=>
int(1)
["y"]=>
int(2)
}

### Considerations for implementations

A `record` cannot be named after an existing `record`, `class` or `function`. This is because defining a `record`
creates both a `class` and a `function` with the same name.

Comment thread
withinboredom marked this conversation as resolved.
Outdated
Comment thread
withinboredom marked this conversation as resolved.
Outdated
### Auto loading
Comment thread
withinboredom marked this conversation as resolved.
Outdated

As invoking a record value by its name looks remarkably similar to calling a function,
and PHP has no function autoloader, auto loading will not be supported in this implementation.
If function auto loading were to be implemented in the future, an autoloader could locate the `record` and autoload it.
The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records.

## Backward Incompatible Changes

No backward incompatible changes.
Comment thread
withinboredom marked this conversation as resolved.
Outdated

## Proposed PHP Version(s)

PHP 8.5

## RFC Impact

### To SAPIs

N/A

### To Existing Extensions

N/A

### To Opcache

Unknown.

### New Constants

None

### php.ini Defaults

None

## Open Issues

Comment thread
withinboredom marked this conversation as resolved.
Todo

## Unaffected PHP Functionality

None.

## Future Scope
Comment thread
withinboredom marked this conversation as resolved.

## Proposed Voting Choices

Include these so readers know where you are heading and can discuss the
proposed voting options.

## Patches and Tests

TBD
Comment thread
withinboredom marked this conversation as resolved.

## Implementation

After the project is implemented, this section should contain

1. the version(s) it was merged into
2. a link to the git commit(s)
3. a link to the PHP manual entry for the feature
4. a link to the language specification section (if any)

Comment thread
withinboredom marked this conversation as resolved.
Outdated
## References

Links to external references, discussions or RFCs
Comment thread
withinboredom marked this conversation as resolved.
Outdated

## Rejected Features

Keep this updated with features that were discussed on the mail lists.
Comment thread
withinboredom marked this conversation as resolved.
Outdated
Empty file removed published/.gitkeep
Empty file.
Loading