diff --git a/.coderabbit.yml b/.coderabbit.yml index 20120d7..d21bb16 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -3,7 +3,7 @@ tone_instructions: '' early_access: false enable_free_tier: true reviews: - profile: chill + profile: assertive request_changes_workflow: false high_level_summary: true high_level_summary_placeholder: '@coderabbitai summary' @@ -18,7 +18,7 @@ reviews: path_instructions: [ { "path": "drafts/*.md", - "instructions": "These are PHP RFC's to change the PHP language. Be constructive but critical in how it may change the language" + "instructions": "These are PHP RFC's to change the PHP language. Do not just consider the grammar of the text, but consider how it might change the language. For example, if a new feature is being added, consider how it might be used, and propose better ideas if you have them." } ] abort_on_close: true diff --git a/Makefile b/Makefile index 33420e8..fac4dff 100644 --- a/Makefile +++ b/Makefile @@ -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 $@" diff --git a/drafts/.gitkeep b/drafts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md new file mode 100644 index 0000000..3c6635b --- /dev/null +++ b/drafts/function-autoloading.md @@ -0,0 +1,173 @@ +# PHP RFC: Function Autoloading v4 + +* Version: 1.0 +* Date: 2024-08-15 +* Author: Robert Landers, landers.robert@gmail.com +* Status: Under Discussion (or Accepted or Declined) +* First Published at: + +## Introduction + +The topic of supporting function autoloading was brought up many times in the past, this RFC introduces a potential +implementation which would be consistent with what we have for autoloading classes. + +## Proposal + +Before getting into the details, +there are a few terms worth acknowledging so that the proposal can be easily discussed without getting confused: + +1. **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. +2. **Undefined function**: A function that the engine does not have knowledge of. +3. **Function autoloading**: The process of loading a function that is not defined. +4. **Written function**: A function that exists in a file that the engine may or may not have knowledge of. +5. **Local scope**: The current namespace +6. **Global scope**: The global namespace (`\`) + +The suggested change would be pretty straightforward and backwards-compatible: + +1. Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. +2. Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. +3. The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., + SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) +4. Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. +5. Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. + +There won’t be any changes to the current autoloading mechanism when it comes to classes. +However, if a function + +1. is called in a fully qualified form (e.g., a `use` statement or `\` prefix is used), +2. is not defined, +3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and +SPL_AUTOLOAD_FUNCTION as the second parameter. + +However, if a function + +1. is called in an unqualified form (e.g., `strlen()`), +2. is not defined locally +3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the current namespace prepended to the function name. +If the autoloader chooses to look up the "basename" of the function, it may do so. +If the function is still undefined in the local scope, +then it will fall back to the global scope—unless the local scope is the global scope. +The function autoloader will not be called again. + +This provides an opportunity +for an autoloader to check for the existence of a function in the local scope and define it, +as well as defer to the global scope if it is not defined. + +Example "`PSR-4-style`" (except the last part of the namespace is the file it is in) function autoloader: + +```php + + * Yes + * No + + +## Patches and Tests + +Not yet. + +## Implementation + +After the project is implemented, this section should contain - the +version(s) it was merged into - a link to the git commit(s) - a link to +the PHP manual entry for the feature - a link to the language +specification section (if any) + +## References + +- [autofunc](https://wiki.php.net/rfc/autofunc): This heavily influenced this RFC. (declined in 2011) +- [function_autoloading](https://wiki.php.net/rfc/function_autoloading): This RFC was declined in 2011. +- [function_autoloading_v2](https://wiki.php.net/rfc/function_autoloading2): This RFC was declined in 2012. + +Thank you for all of those that contributed to the discussions back then. I hope that this RFC will be successful. + +## Rejected Features + +Keep this updated with features that were discussed on the mail lists. diff --git a/drafts/records.md b/drafts/records.md new file mode 100644 index 0000000..16b6a09 --- /dev/null +++ b/drafts/records.md @@ -0,0 +1,703 @@ +# PHP RFC: Records + +- Version: 0.9 +- Date: 2024-07-19 +- Author: Robert Landers, landers.robert@gmail.com, rob@bottled.codes +- Status: Under Discussion (or Accepted or Declined) +- First Published at: + +## Introduction + +This RFC proposes the introduction of `record` objects, which are immutable classes +with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). + +### Value objects + +Value objects are immutable objects that represent a value. +They’re used to store values with a different semantic by wrapping their technical value, adding additional context. +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 where a function accepts an integer as a user ID, +and the ID is accidentally set to a nonsensical value: + +```php +function updateUserRole(int $userId, string $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; +// ... +$uid = 5; // accidentally sets uid to an unrelated integer +// ... +updateUserRole($uid, 'admin'); // accidental passes a nonsensical value for uid +``` + +Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. +Further, **readonly classes** have many edge cases and are rather unwieldy. + +#### 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 at an updated example using a `record` type for `UserId`. +Thus, if someone were to pass an `int` to `updateUserRole`, it would throw an error: + +```php +record UserId(int $id); + +function updateUserRole(UserId $userId, string $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; // $uid is a UserId object +// ... +$uid = 5; +// ... +updateUserRole($uid, 'admin'); // This will throw a TypeError +``` + +Now, if `$uid` is accidentally set to an integer, +the call to `updateUserRole` will throw a `TypeError` +because the function expects a `UserId` object instead of a plain integer. + +## Proposal + +This RFC proposes the introduction of a `record` keyword in PHP to define immutable value objects. +These objects will allow properties to be initialized concisely +and will provide built-in methods for common operations such as modifying properties, +performing equality checks, and using a function-like instantiation syntax. +Records can implement interfaces and use traits but can’t extend other records or classes; +composition is allowed, however. + +### Syntax and semantics + +#### Definition + +A **record** is defined by the keyword `record`, +followed by the name of its type (e.g., `UserId`), +and then must list one or more typed parameters (e.g., `int $id`) that become properties of the record. +A parameter may provide `private` or `public` modifiers, but are `public` when not specified. +This is referred to as the "inline constructor." + +A **record** may optionally implement an interface using the `implements` keyword, +which may optionally be followed by a record body enclosed in curly braces `{}`. + +A **record** may not extend another record or class. + +A **record** may contain a traditional constructor with zero arguments to perform further initialization. + +A **record** body may contain property hooks, methods, and use traits. + +A **record** body may also declare properties whose values are only mutable during a constructor call. +At any other time, the property is immutable. + +A **record** body may also contain static methods and properties, +which behave identically to static methods and properties in classes. +They may be accessed using the `::` operator. + +As an example, the following code defines a **record** named `Pigment` to represent a color, +`StockPaint` to represent paint colors in stock, +and `PaintBucket` to represent a collection of stock paints mixed together. +The actual behavior isn’t important, but illustrates the syntax and semantics of records. + +``` php +namespace Paint; + +// Define a record with several primary color properties +record Pigment(int $red, int $yellow, int $blue) { + + // property hooks are allowed + public string $hexValue { + get => sprintf("#%02x%02x%02x", $this->red, $this->yellow, $this->blue), + } + + // methods are allowed + public function mix(Pigment $other, float $amount): Pigment { + 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 + ); + } + + // all properties are mutable in constructors + public function __construct() { + $this->red = max(0, min(255, $this->red)); + $this->yellow = max(0, min(255, $this->yellow)); + $this->blue = max(0, min(255, $this->blue)); + } + + public function with() { + // prevent the creation of a new Pigment from an existing pigment + throw new \LogicException("Cannot create a new Pigment from an existing pigment"); + } +} + +// simple records do not need to define a body +record StockPaint(Pigment $color, float $volume); + +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 much like a class, +as the behavior of the two is very similar, +assisting in migrating from one implementation to another: + +```php +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +``` + +Records are instantiated in a function format, with `&` prepended. +This provides visual feedback that a record is being created instead of a function call. + +```php +$black = &Pigment(0, 0, 0); +$white = &Pigment(255, 255, 255); +$blackPaint = &StockPaint($black, 1); +$whitePaint = &StockPaint($white, 1); +$bucket = &PaintBucket(); + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + +assert($gray === $grey); // true +``` + +#### Optional parameters and default values + +A `record` can also be defined with optional parameters that are set if omitted during instantiation. + +One or more properties defined in the inline constructor may have a default value +declared using the same syntax and rules as any other default parameter in methods/functions. +If a property has a default value, +it is optional when instantiating the record, and PHP will assign the default value to the property if omitted. + +``` 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 make records more useful, the RFC proposes generating a `with` method for each record. +This method allows for partial updates to the properties, +creating a new instance of the record with the specified properties updated. + +##### How the with method works + +**Named arguments** + +The `with` method accepts only named arguments defined in the inline constructor. +Properties not defined in the inline constructor can’t be updated by this method. + +**Variadic arguments** + +Variadic arguments from the inline constructor don’t require named arguments in the `with` method. +However, mixing named and variadic arguments in the same `with` method call is not allowed by PHP syntax. + +Using named arguments: + +```php +record UserId(int $id) { + public string $serialNumber; + + public function __construct() { + $this->serialNumber = "U{$this->id}"; + } +} + +$userId = &UserId(1); +$otherId = $userId->with(2); // Fails: Named arguments must be used +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor +$otherId = $userId->with(id: 2); // Success: id is updated +``` + +Using variadic arguments: + +```php +record Vector(int $dimensions, int ...$values); + +$vector = &Vector(3, 1, 2, 3); +$vector = $vector->with(dimensions: 4); // Success: values are updated +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values +``` + +##### Custom with method + +A developer may define their own `with` method if they choose, +and reference the generated `with` method using `parent::with()`. +This allows a developer to define policies or constraints on how data can change from instance to instance. + +``` php +record Planet(string $name, int $population) { + // create a with method that only accepts population updates + public function with(int $population): Planet { + return parent::with(population: $population); + } +} +$pluto = Planet("Pluto", 0); +// we made it! +$pluto = $pluto->with(population: 1); +// and then we changed the name +$mickey = $pluto->with(name: "Mickey"); // Error: no named argument for population +``` + +#### Constructors + +A **record** has two types of constructors: the inline constructor and the traditional constructor. + +The inline constructor is always required and must define at least one parameter. +The traditional constructor is optional and can be used for further initialization logic, +but must not accept any arguments. + +When a traditional constructor exists and is called, +the properties are already initialized to the values from the inline constructor +and are mutable until the end of the method, +at which point they become immutable. + +```php +// Inline constructor defining two properties +record User(string $name, string $emailAddress) { + public string $id; + + // Traditional constructor + public function __construct() { + if (!is_valid_email($this->emailAddress)) { + throw new InvalidArgumentException("Invalid email address"); + } + + $this->id = hash('sha256', $this->emailAddress); + $this->name = ucwords($this->name); + // all properties are now immutable + } +} +``` + +### Implementing Interfaces + +A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: + +```php +interface Vehicle {} + +interface Car extends Vehicle { + public function drive(): void; +} + +interface SpaceShip extends Vehicle { + public function launch(): void; +} + +record FancyCar(string $model) implements Car { + public function drive(): void { + echo "Driving a Fancy Car {$this->model}"; + } +} + +record SpaceCar(string $model) implements Car, SpaceShip { + public function drive(): void { + echo "Driving a Space Car {$this->model}"; + } + + public function launch(): void { + echo "Launching a Space Car {$this->model}"; + } +} + +record Submarine(string $model) implements Vehicle { + use Submersible; +} + +record TowTruct(string $model, private Car $towing) implements Car { + use Towable; +} +``` + +### Mental models and how it works + +From the perspective of a developer, declaring a record declares an object with the same name. +The developer can consider the record function (the inline constructor) +as a factory function that creates a new object or retrieves an existing object from an array. + +For example, this would be a valid mental model for a Point record: + +```php +record Point(int $x, int $y) { + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + + public function add(Point $point): Point { + return &Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +// similar to declaring the following function and class + +// used during construction to allow mutability +class Point_Implementation { + public int $x; + public int $y; + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +// used to enforce immutability but has nearly the same implementation +readonly class Point { + public float $magnitude; + + public function __construct(public int $x, public int $y) {} + + public function with(...$parameters): self { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +function Point(int $x, int $y): Point { + static $points = []; + + $key = hash_object($mutablePoint); + if ($points[$key] ?? null) { + // return an existing point + return $points[$key]; + } + + // create a new point + $reflector = new \ReflectionClass(Point_Implementation::class); + $mutablePoint = $reflector->newInstanceWithoutConstructor(); + $mutablePoint->x = $x; + $mutablePoint->y = $y; + $mutablePoint->__construct(); + + // copy properties to an immutable Point and return it + $point = new Point($mutablePoint->x, $mutablePoint->y); + $point->magnitude = $mutablePoint->magnitude; + return $points[$key] = $point; +} +``` + +In reality, this is quite different from how it works in the engine, +but this provides a mental model of how behavior should be expected to work. + +### 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’re no +longer referenced. + +``` php +$point1 = &Point(3, 4); +$point2 = $point1; // No data duplication, $point2 references the same data as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 + +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance +$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4 +``` + +#### Cloning and with() + +Calling `clone` on a `record` results in the same record object being returned. As it is a "value" object, it +represents a value and is the same thing as saying `clone 3`—you expect to get back a `3`. + +If `->with()` is called with no arguments, a warning will be emitted, as this is most likely a mistake. + +### Serialization and deserialization + +Records are fully serializable and deserializable, even when nested. + +```php +record Single(string $value); +record Multiple(string $value1, string $value2); + +echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +echo $multiple = serialize(&Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}" + +echo unserialize($single) === &Single('value'); // Outputs: true +echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true +``` + +If a record contains objects or values that are unserializable, the record will not be serializable. + +### 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, which is currently undefined. + +#### Non-trivial values + +For non-trivial values (e.g., objects, closures, resources, etc.), +the `===` operator will return `true` if the two operands reference the same instances. + +For example, if two different DateTime records reference the exact same date +and are stored in a record, the records will not be considered equal: + +```php +$date1 = DateTime('2024-07-19'); +$date2 = DateTime('2024-07-19'); + +record Date(DateTime $date); + +$dateRecord1 = Date($date1); +$dateRecord2 = Date($date2); + +echo $dateRecord1 === $dateRecord2; // Outputs: false +``` + +However, +this can be worked around by being a bit creative (see: mental model) +as only the values passed in the constructor are compared: + +```php +record Date(string $date) { + public DateTime $datetime; + + public function __construct() { + $this->datetime = new DateTime($this->date); + } +} + +$date1 = &Date('2024-07-19'); +$date2 = &Date('2024-07-19'); + +echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true +``` + +### Type hinting + +A `\Record` interface will be added to the engine to allow type hinting for records. +All records implement this interface. + +```php +function doSomething(\Record $record): void { + // ... +} +``` + +The only method on the interface is `with`, which is a variadic method that accepts named arguments and returns `self`. + +### Reflection + +A new reflection class will be added to support records: +`ReflectionRecord` which will inherit from `ReflectionClass` and add a few additional methods: + +- `ReflectionRecord::finalizeRecord(object $instance): Record`: Finalizes a record under construction, making it immutable. +- `ReflectionRecord::isRecord(mixed $object): bool`: Returns `true` if the object is a record, and `false` otherwise. +- `ReflectionRecord::getInlineConstructor(): ReflectionFunction`: Returns the inline constructor of the record as `ReflectionFunction`. +- `ReflectionRecord::getTraditionalConstructor(): ReflectionMethod`: Returns the traditional constructor of the record as `ReflectionMethod`. +- `ReflectionRecord::makeMutable(Record $instance): object`: Returns a new record instance with the properties mutable. +- `ReflectionRecord::isMutable(Record $instance): bool`: Returns `true` if the record is mutable, and `false` otherwise. + +Using `ReflectionRecord` will allow developers to inspect records, their properties, and methods, +as well as create new instances for testing or custom deserialization. + +Attempting to use `ReflectionClass` or `ReflectionFunction` on a record will throw a `ReflectionException` exception. + +#### finalizeRecord() + +The `finalizeRecord()` method is used to make a record immutable and look up its value in the internal cache, +returning an instance that represents the finalized record. + +Calling `finalizeRecord()` on a record that has already been finalized will return the same instance. +Attempting to finalize a regular object will throw a `ReflectionException`. + +#### isRecord() + +The `isRecord()` method is used to determine if an object is a record. +It returns `true` if the object is a finalized record. + +#### getInlineConstructor() + +The `getInlineConstructor()` method is used to get the inline constructor of a record as a `ReflectionFunction`. +This can be used to inspect inlined properties and their types. + +Invoking the `invoke()` method on the `ReflectionFunction` will create a finalized record. + +#### getTraditionalConstructor() + +The `getTraditionalConstructor()` method is used +to get the traditional constructor of a record as a `ReflectionMethod`. +This can be useful to inspect the constructor for further initialization. + +Invoking the `invoke()` method on the `ReflectionMethod` on a finalized record will throw an exception. + +#### makeMutable() + +The `makeMutable()` method is used to create a new instance of a record with mutable properties. +The returned instance doesn’t provide any value semantics +and should only be used for testing purposes or when there is no other option. + +A mutable record can be finalized again using `finalizeRecord()`. +A mutable record will not be considered a record by `isRecord()` or implement the `\Record` interface. +It is a regular object with the same properties and methods as the record. +For example, `var_dump()` will output `object` instead of `record`. + +#### isMutable() + +The `isMutable()` method is used +to determine if a record has been made mutable via `makeMutable()` or otherwise not yet finalized. + +#### Custom deserialization example + +In cases where custom deserialization is required, +a developer can use `ReflectionRecord` to manually construct a new instance of a record. + +```php +record Seconds(int $seconds); + +$example = &Seconds(5); + +$reflector = new ReflectionRecord(Seconds::class); +$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object +$expiration->seconds = 5; +assert($example !== $expiration); // true +$expiration = $reflector->finalizeRecord($expiration); +assert($example === $expiration); // true +``` + +### var_dump + +When passed an instance of a record the `var_dump()` function will output the same +as if an equivalent object were passed — +e.g., both having the same properties — except the output generated will replace the prefix text "object" +with the text "record." + +```txt +record(Point)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(2) +} +``` + +### Considerations for implementations + +A `record` cannot share its name with an existing `record`, +`class`, `interface`, `trait`, or `function`, just like a class. + +### Autoloading + +Records will be autoloaded in the same way as classes. + +### New Functions + +- `record_exists` will return `true` if a record exists and `false` otherwise. It has the same signature as `class_exists`. + +## Backward Incompatible Changes + +To avoid conflicts with existing code, +the `record` keyword will be handled similarly to `enum` to prevent backward compatibility issues. + +Since `&` is currently a syntax error when prefixed on a function call, +it will be used to denote a record instantiation. + +## 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 + +To-do + +## Unaffected PHP Functionality + +None. + +## Future Scope + +- Records for "record-like" types, such as DateTime, DateInterval, and others. + +## Proposed Voting Choices + +2/3 majority. + +## Patches and Tests + +TBD + +## Implementation + +To be completed during a later phase of discussion. + +## References + +- [Value semantics](https://en.wikipedia.org/wiki/Value_semantics) + +## Rejected Features + +TBD diff --git a/published/.gitkeep b/published/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt new file mode 100644 index 0000000..2f2d4ad --- /dev/null +++ b/published/function-autoloading.ptxt @@ -0,0 +1,144 @@ +====== PHP RFC: Function Autoloading v4 ====== + + * Version: 1.0 + * Date: 2024-08-15 + * Author: Robert Landers, + * Status: Under Discussion (or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/function_autoloading4 + +===== Introduction ===== + +The topic of supporting function autoloading was brought up many times in the past, this RFC introduces a potential implementation which would be consistent with what we have for autoloading classes. + +===== Proposal ===== + +Before getting into the details, there are a few terms worth acknowledging so that the proposal can be easily discussed without getting confused: + + - **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. + - **Undefined function**: A function that the engine does not have knowledge of. + - **Function autoloading**: The process of loading a function that is not defined. + - **Written function**: A function that exists in a file that the engine may or may not have knowledge of. + - **Local scope**: The current namespace + - **Global scope**: The global namespace (''%%\%%'') + +The suggested change would be pretty straightforward and backwards-compatible: + + - Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. + - Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. + - The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) + - Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. + - Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. + +There won’t be any changes to the current autoloading mechanism when it comes to classes. However, if a function + + - is called in a fully qualified form (e.g., a ''%%use%%'' statement or ''%%\%%'' prefix is used), + - is not defined, + - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and SPL_AUTOLOAD_FUNCTION as the second parameter. + +However, if a function + + - is called in an unqualified form (e.g., ''%%strlen()%%''), + - is not defined locally + - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. If the function is still undefined in the local scope, then it will fall back to the global scope—unless the local scope is the global scope. The function autoloader will not be called again. + +This provides an opportunity for an autoloader to check for the existence of a function in the local scope and define it, as well as defer to the global scope if it is not defined. + +Example "''%%PSR-4-style%%''" (except the last part of the namespace is the file it is in) function autoloader: + + + + +Performance-wise, this should have minimal impact on existing codebases as there is no default function autoloader. + +For codebases that want to take advantage of function autoloading, it may be desirable to stick with FQNs for functions and/or employ caches and other techniques where possible. + +==== spl_autoload ==== + +''%%spl_autoload%%'''s second argument will be updated to accept ''%%int|string|null%%'' as the second parameter so that it can use the new callback signature. If the second parameter is an int, and it is not ''%%SPL_AUTOLOAD_CLASS%%'', an ''%%Error%%'' is thrown: 'Default autoloader can only load classes.' + +There will not be a default function autoloader. + +==== spl_autoload_call ==== + +The ''%%spl_autoload_call%%'' function will be modified to accept a second parameter of one, (but not both) of the new constants, with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to ''%%$name%%'' to reflect that it can be a class or function name. + + +spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader +spl_autoload_call('\Some\func'); // Calls the class autoloader +spl_autoload_call('Some\func', SPL_AUTOLOAD_CLASS); // Calls the class autoloader +spl_autoload_call('Some\func'); // Calls the class autoloader +spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: Cannot autoload multiple types + + +If the user wants to call multiple autoloaders, they can do so manually. + +==== function_exists ==== + +The ''%%function_exists%%'' function will be updated to include a boolean option (''%%$autoload%%'') as the second parameter, which will default to ''%%true%%''. If set to ''%%true%%'', the function autoloader will be called if the function is not defined, otherwise, it will not be called. + +===== Backward Incompatible Changes ===== + +==== Mismatched arguments ==== + +If an autoloader was registered that can accept more than one argument, it may fail or perform unexpected behavior when it receives a second argument of ''%%SPL_AUTOLOAD_CLASS%%''. + +===== Proposed PHP Version(s) ===== + +8.5 or later. + +===== RFC Impact ===== + +==== To Opcache ==== + +To be determined. + +==== New Constants ==== + +Two new constants will be added to the SPL extension: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. + +===== Open Issues ===== + +To be determined. + +===== Future Scope ===== + +Potentially, constants and stream wrappers can be added in a similar fashion. + +===== Proposed Voting Choices ===== + + +===== Patches and Tests ===== + +Not yet. + +===== Implementation ===== + +After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) + +===== References ===== + + * [[https://wiki.php.net/rfc/autofunc|autofunc]]: This heavily influenced this RFC. (declined in 2011) + * [[https://wiki.php.net/rfc/function_autoloading|function_autoloading]]: This RFC was declined in 2011. + * [[https://wiki.php.net/rfc/function_autoloading2|function_autoloading_v2]]: This RFC was declined in 2012. + +Thank you for all of those that contributed to the discussions back then. I hope that this RFC will be successful. + +===== Rejected Features ===== + +Keep this updated with features that were discussed on the mail lists. diff --git a/published/records.ptxt b/published/records.ptxt new file mode 100644 index 0000000..695b7ba --- /dev/null +++ b/published/records.ptxt @@ -0,0 +1,624 @@ +====== PHP RFC: Records ====== + + * Version: 0.9 + * Date: 2024-07-19 + * Author: Robert Landers, , + * Status: Under Discussion (or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/records + +===== Introduction ===== + +This RFC proposes the introduction of ''%%record%%'' objects, which are immutable classes with [[https://en.wikipedia.org/wiki/Value_semantics|value semantics]]. + +==== Value objects ==== + +Value objects are immutable objects that represent a value. They’re used to store values with a different semantic by wrapping their technical value, adding additional context. 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 where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: + + +function updateUserRole(int $userId, string $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; +// ... +$uid = 5; // accidentally sets uid to an unrelated integer +// ... +updateUserRole($uid, 'admin'); // accidental passes a nonsensical value for uid + + +Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. Further, **readonly classes** have many edge cases and are rather unwieldy. + +=== 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 at an updated example using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error: + + +record UserId(int $id); + +function updateUserRole(UserId $userId, string $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; // $uid is a UserId object +// ... +$uid = 5; +// ... +updateUserRole($uid, 'admin'); // This will throw a TypeError + + +Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUserRole%%'' will throw a ''%%TypeError%%'' because the function expects a ''%%UserId%%'' object instead of a plain integer. + +===== Proposal ===== + +This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties, performing equality checks, and using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. + +==== Syntax and semantics ==== + +=== Definition === + +A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' when not specified. This is referred to as the "inline constructor." + +A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''. + +A **record** may not extend another record or class. + +A **record** may contain a traditional constructor with zero arguments to perform further initialization. + +A **record** body may contain property hooks, methods, and use traits. + +A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time, the property is immutable. + +A **record** body may also contain static methods and properties, which behave identically to static methods and properties in classes. They may be accessed using the ''%%::%%'' operator. + +As an example, the following code defines a **record** named ''%%Pigment%%'' to represent a color, ''%%StockPaint%%'' to represent paint colors in stock, and ''%%PaintBucket%%'' to represent a collection of stock paints mixed together. The actual behavior isn’t important, but illustrates the syntax and semantics of records. + + +namespace Paint; + +// Define a record with several primary color properties +record Pigment(int $red, int $yellow, int $blue) { + + // property hooks are allowed + public string $hexValue { + get => sprintf("#%02x%02x%02x", $this->red, $this->yellow, $this->blue), + } + + // methods are allowed + public function mix(Pigment $other, float $amount): Pigment { + 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 + ); + } + + // all properties are mutable in constructors + public function __construct() { + $this->red = max(0, min(255, $this->red)); + $this->yellow = max(0, min(255, $this->yellow)); + $this->blue = max(0, min(255, $this->blue)); + } + + public function with() { + // prevent the creation of a new Pigment from an existing pigment + throw new \LogicException("Cannot create a new Pigment from an existing pigment"); + } +} + +// simple records do not need to define a body +record StockPaint(Pigment $color, float $volume); + +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 much like a class, as the behavior of the two is very similar, assisting in migrating from one implementation to another: + + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + + +Records are instantiated in a function format, with ''%%&%%'' prepended. This provides visual feedback that a record is being created instead of a function call. + + +$black = &Pigment(0, 0, 0); +$white = &Pigment(255, 255, 255); +$blackPaint = &StockPaint($black, 1); +$whitePaint = &StockPaint($white, 1); +$bucket = &PaintBucket(); + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + +assert($gray === $grey); // true + + +=== Optional parameters and default values === + +A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. + +One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter in methods/functions. If a property has a default value, it is optional when instantiating the record, and PHP will assign the default value to the property if omitted. + + +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 make records more useful, the RFC proposes generating a ''%%with%%'' method for each record. This method allows for partial updates to the properties, creating a new instance of the record with the specified properties updated. + +== How the with method works == + +**Named arguments** + +The ''%%with%%'' method accepts only named arguments defined in the inline constructor. Properties not defined in the inline constructor can’t be updated by this method. + +**Variadic arguments** + +Variadic arguments from the inline constructor don’t require named arguments in the ''%%with%%'' method. However, mixing named and variadic arguments in the same ''%%with%%'' method call is not allowed by PHP syntax. + +Using named arguments: + + +record UserId(int $id) { + public string $serialNumber; + + public function __construct() { + $this->serialNumber = "U{$this->id}"; + } +} + +$userId = &UserId(1); +$otherId = $userId->with(2); // Fails: Named arguments must be used +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor +$otherId = $userId->with(id: 2); // Success: id is updated + + +Using variadic arguments: + + +record Vector(int $dimensions, int ...$values); + +$vector = &Vector(3, 1, 2, 3); +$vector = $vector->with(dimensions: 4); // Success: values are updated +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values + + +== Custom with method == + +A developer may define their own ''%%with%%'' method if they choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data can change from instance to instance. + + +record Planet(string $name, int $population) { + // create a with method that only accepts population updates + public function with(int $population): Planet { + return parent::with(population: $population); + } +} +$pluto = Planet("Pluto", 0); +// we made it! +$pluto = $pluto->with(population: 1); +// and then we changed the name +$mickey = $pluto->with(name: "Mickey"); // Error: no named argument for population + + +=== Constructors === + +A **record** has two types of constructors: the inline constructor and the traditional constructor. + +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but must not accept any arguments. + +When a traditional constructor exists and is called, the properties are already initialized to the values from the inline constructor and are mutable until the end of the method, at which point they become immutable. + + +// Inline constructor defining two properties +record User(string $name, string $emailAddress) { + public string $id; + + // Traditional constructor + public function __construct() { + if (!is_valid_email($this->emailAddress)) { + throw new InvalidArgumentException("Invalid email address"); + } + + $this->id = hash('sha256', $this->emailAddress); + $this->name = ucwords($this->name); + // all properties are now immutable + } +} + + +==== Implementing Interfaces ==== + +A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: + + +interface Vehicle {} + +interface Car extends Vehicle { + public function drive(): void; +} + +interface SpaceShip extends Vehicle { + public function launch(): void; +} + +record FancyCar(string $model) implements Car { + public function drive(): void { + echo "Driving a Fancy Car {$this->model}"; + } +} + +record SpaceCar(string $model) implements Car, SpaceShip { + public function drive(): void { + echo "Driving a Space Car {$this->model}"; + } + + public function launch(): void { + echo "Launching a Space Car {$this->model}"; + } +} + +record Submarine(string $model) implements Vehicle { + use Submersible; +} + +record TowTruct(string $model, private Car $towing) implements Car { + use Towable; +} + + +==== Mental models and how it works ==== + +From the perspective of a developer, declaring a record declares an object with the same name. The developer can consider the record function (the inline constructor) as a factory function that creates a new object or retrieves an existing object from an array. + +For example, this would be a valid mental model for a Point record: + + +record Point(int $x, int $y) { + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + + public function add(Point $point): Point { + return &Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +// similar to declaring the following function and class + +// used during construction to allow mutability +class Point_Implementation { + public int $x; + public int $y; + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +// used to enforce immutability but has nearly the same implementation +readonly class Point { + public float $magnitude; + + public function __construct(public int $x, public int $y) {} + + public function with(...$parameters): self { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } +} + +function Point(int $x, int $y): Point { + static $points = []; + + $key = hash_object($mutablePoint); + if ($points[$key] ?? null) { + // return an existing point + return $points[$key]; + } + + // create a new point + $reflector = new \ReflectionClass(Point_Implementation::class); + $mutablePoint = $reflector->newInstanceWithoutConstructor(); + $mutablePoint->x = $x; + $mutablePoint->y = $y; + $mutablePoint->__construct(); + + // copy properties to an immutable Point and return it + $point = new Point($mutablePoint->x, $mutablePoint->y); + $point->magnitude = $mutablePoint->magnitude; + return $points[$key] = $point; +} + + +In reality, this is quite different from how it works in the engine, but this provides a mental model of how behavior should be expected to work. + +==== 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’re no longer referenced. + + +$point1 = &Point(3, 4); +$point2 = $point1; // No data duplication, $point2 references the same data as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 + +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance +$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4 + + +=== Cloning and with() === + +Calling ''%%clone%%'' on a ''%%record%%'' results in the same record object being returned. As it is a "value" object, it represents a value and is the same thing as saying ''%%clone 3%%''—you expect to get back a ''%%3%%''. + +If ''%%->with()%%'' is called with no arguments, a warning will be emitted, as this is most likely a mistake. + +==== Serialization and deserialization ==== + +Records are fully serializable and deserializable, even when nested. + + +record Single(string $value); +record Multiple(string $value1, string $value2); + +echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +echo $multiple = serialize(&Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}" + +echo unserialize($single) === &Single('value'); // Outputs: true +echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true + + +If a record contains objects or values that are unserializable, the record will not be serializable. + +==== 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, which is currently undefined. + +=== Non-trivial values === + +For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same instances. + +For example, if two different DateTime records reference the exact same date and are stored in a record, the records will not be considered equal: + + +$date1 = DateTime('2024-07-19'); +$date2 = DateTime('2024-07-19'); + +record Date(DateTime $date); + +$dateRecord1 = Date($date1); +$dateRecord2 = Date($date2); + +echo $dateRecord1 === $dateRecord2; // Outputs: false + + +However, this can be worked around by being a bit creative (see: mental model) as only the values passed in the constructor are compared: + + +record Date(string $date) { + public DateTime $datetime; + + public function __construct() { + $this->datetime = new DateTime($this->date); + } +} + +$date1 = &Date('2024-07-19'); +$date2 = &Date('2024-07-19'); + +echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true + + +==== Type hinting ==== + +A ''%%\Record%%'' interface will be added to the engine to allow type hinting for records. All records implement this interface. + + +function doSomething(\Record $record): void { + // ... +} + + +The only method on the interface is ''%%with%%'', which is a variadic method that accepts named arguments and returns ''%%self%%''. + +==== Reflection ==== + +A new reflection class will be added to support records: ''%%ReflectionRecord%%'' which will inherit from ''%%ReflectionClass%%'' and add a few additional methods: + + * ''%%ReflectionRecord::finalizeRecord(object $instance): Record%%'': Finalizes a record under construction, making it immutable. + * ''%%ReflectionRecord::isRecord(mixed $object): bool%%'': Returns ''%%true%%'' if the object is a record, and ''%%false%%'' otherwise. + * ''%%ReflectionRecord::getInlineConstructor(): ReflectionFunction%%'': Returns the inline constructor of the record as ''%%ReflectionFunction%%''. + * ''%%ReflectionRecord::getTraditionalConstructor(): ReflectionMethod%%'': Returns the traditional constructor of the record as ''%%ReflectionMethod%%''. + * ''%%ReflectionRecord::makeMutable(Record $instance): object%%'': Returns a new record instance with the properties mutable. + * ''%%ReflectionRecord::isMutable(Record $instance): bool%%'': Returns ''%%true%%'' if the record is mutable, and ''%%false%%'' otherwise. + +Using ''%%ReflectionRecord%%'' will allow developers to inspect records, their properties, and methods, as well as create new instances for testing or custom deserialization. + +Attempting to use ''%%ReflectionClass%%'' or ''%%ReflectionFunction%%'' on a record will throw a ''%%ReflectionException%%'' exception. + +=== finalizeRecord() === + +The ''%%finalizeRecord()%%'' method is used to make a record immutable and look up its value in the internal cache, returning an instance that represents the finalized record. + +Calling ''%%finalizeRecord()%%'' on a record that has already been finalized will return the same instance. Attempting to finalize a regular object will throw a ''%%ReflectionException%%''. + +=== isRecord() === + +The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a finalized record. + +=== getInlineConstructor() === + +The ''%%getInlineConstructor()%%'' method is used to get the inline constructor of a record as a ''%%ReflectionFunction%%''. This can be used to inspect inlined properties and their types. + +Invoking the ''%%invoke()%%'' method on the ''%%ReflectionFunction%%'' will create a finalized record. + +=== getTraditionalConstructor() === + +The ''%%getTraditionalConstructor()%%'' method is used to get the traditional constructor of a record as a ''%%ReflectionMethod%%''. This can be useful to inspect the constructor for further initialization. + +Invoking the ''%%invoke()%%'' method on the ''%%ReflectionMethod%%'' on a finalized record will throw an exception. + +=== makeMutable() === + +The ''%%makeMutable()%%'' method is used to create a new instance of a record with mutable properties. The returned instance doesn’t provide any value semantics and should only be used for testing purposes or when there is no other option. + +A mutable record can be finalized again using ''%%finalizeRecord()%%''. A mutable record will not be considered a record by ''%%isRecord()%%'' or implement the ''%%\Record%%'' interface. It is a regular object with the same properties and methods as the record. For example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''. + +=== isMutable() === + +The ''%%isMutable()%%'' method is used to determine if a record has been made mutable via ''%%makeMutable()%%'' or otherwise not yet finalized. + +=== Custom deserialization example === + +In cases where custom deserialization is required, a developer can use ''%%ReflectionRecord%%'' to manually construct a new instance of a record. + + +record Seconds(int $seconds); + +$example = &Seconds(5); + +$reflector = new ReflectionRecord(Seconds::class); +$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object +$expiration->seconds = 5; +assert($example !== $expiration); // true +$expiration = $reflector->finalizeRecord($expiration); +assert($example === $expiration); // true + + +==== var_dump ==== + +When passed an instance of a record the ''%%var_dump()%%'' function will output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." + + +record(Point)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(2) +} + + +==== Considerations for implementations ==== + +A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', ''%%interface%%'', ''%%trait%%'', or ''%%function%%'', just like a class. + +==== Autoloading ==== + +Records will be autoloaded in the same way as classes. + +==== New Functions ==== + + * ''%%record_exists%%'' will return ''%%true%%'' if a record exists and ''%%false%%'' otherwise. It has the same signature as ''%%class_exists%%''. + +===== Backward Incompatible Changes ===== + +To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues. + +Since ''%%&%%'' is currently a syntax error when prefixed on a function call, it will be used to denote a record instantiation. + +===== 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 ===== + +To-do + +===== Unaffected PHP Functionality ===== + +None. + +===== Future Scope ===== + + * Records for "record-like" types, such as DateTime, DateInterval, and others. + +===== Proposed Voting Choices ===== + +2/3 majority. + +===== Patches and Tests ===== + +TBD + +===== Implementation ===== + +To be completed during a later phase of discussion. + +===== References ===== + + * [[https://en.wikipedia.org/wiki/Value_semantics|Value semantics]] + +===== Rejected Features ===== + +TBD diff --git a/published/template.ptxt b/published/template.ptxt index 3f379b3..960ba82 100644 --- a/published/template.ptxt +++ b/published/template.ptxt @@ -7,11 +7,11 @@ This is a suggested template for PHP Request for Comments (RFCs). Change this te Quoting [[http://news.php.net/php.internals/71525|Rasmus]]: > PHP is and should remain: -> + > 1) a pragmatic web-focused language -> + > 2) a loosely typed language -> + > 3) a language which caters to the skill-levels and platforms of a wide range of users Your RFC should move PHP forward following his vision. As [[http://news.php.net/php.internals/66065|said by Zeev Suraski]] "Consider only features which have significant traction to a large chunk of our userbase, and not something that could be useful in some extremely specialized edge cases [...] Make sure you think about the full context, the huge audience out there, the consequences of making the learning curve steeper with every new feature, and the scope of the goodness that those new features bring." diff --git a/src/convert-from-md.sh b/src/convert-from-md.sh index 8fa25dd..d98c6c5 100755 --- a/src/convert-from-md.sh +++ b/src/convert-from-md.sh @@ -1,3 +1,6 @@ #!/bin/sh -docker run -v "$(pwd)":/data --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +docker run -v "$(pwd)":/data --pull always --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +# remove all and tags +sed -i 's///g' "$2" +sed -i 's/<\/HTML>//g' "$2"