This is an old revision of the document!
PHP RFC: Operator Overrides -- Lite Edition
- Version: 0.0
- Date: 2024-06-28
- Author: Robert Landers, landers.robert@gmail.com
- Status: Under Discussion (or Under Discussion or Accepted or Declined)
- First Published at: http://wiki.php.net/rfc/operator_overrides_lite
Introduction
Nearly three years ago, PHP RFC: User Defined Operator Overloads was started before eventually coming to a vote and being declined for multiple reasons, mostly around scope and new syntax. It has come to the author's attention that the GMP object (which represents the GMP extensions representation of a number) was left non-final. In an experimental library, I decided to extend it and just see what happens.
The GMP extension overrides operators in order to provide a clean way to express mathematical constructs, such that when a 2 * StandardSecond is written in this library, a number representing 4 seconds comes out the other side. The author then began investigating how to make it so that instead of a standard GMP object representing 4 seconds, a “time object” would come out the other end.
This RFC aims to embrace the accidental non-final aspect of the \GMP class, allow for a small subset of operator overriding, such that anything that can be backed by a single number can be represented like a number. For example a unit library could allow for multiplying space / time to get velocity, a complex number library, a time library, etc.
Further, this will only be enabled when the GMP extension is enabled/installed.
Proposal
The \GMP class will have the following methods added as protected static:
- add($left, $right): static
- subtract($left, $right): static
- multiply($left, $right): static
- divide($left, $right): static
- mod($left, $right): static
- exp($left, $right): static
- comparable($left, right): bool
There will not be a way to override equals, less-than, greater-than, etc. Thus the overriding class will need to represent itself as a number, however, it can return `false` from `comparable` to prevent itself to being compared to non-sensical types (e.g., “is 5 miles more than 30 seconds” or “is 1 falcon equal to 12 parsecs”).
Binary operations will be handled as such:
- In the case that both types of a binary operation are the base class, no behavior will be changed.
- In the event that one of them is a child class of \GMP, the extension will execute the child class's associated override.
- In the event both of them are child classes of \GMP, the exension will only call the left one. Thus it is the library's responsibility to check that it is compatable with the arguments passed.
Here's a basic implementation example for duration represented in seconds:
class Duration extends \GMP { public function __construct(int|\GMP|float $time) { parent::__construct($time, 10); } private static function assertCompatible($arg): void { if($arg instanceof Time || is_numeric($arg) || (is_object($arg) && class_name($arg) === '\GMP')) { return; } // dummy error message throw new LogicException('Time is not compatible with ' . $arg); } private static function assertNumber($arg): void { if(is_numeric($arg) || (is_object($arg) && class_name($arg) === '\GMP')) { return; } // dummy error message throw new LogicException('Time is not compatible with ' . $arg); } protected static function add($left, $right): static { self::assertCompatible($right); return new self(parent::add($left, $right)); } protected static function subtract($left, $right): static { self::assertCompatible($right); return new self(parent::subtract($left, $right)); } protected static function multiply($left, $right): static { if($left instanceof self) self::assertNumber($right); if($right instanceof self) self::assertNumber($left); return new self(parent::multiply($left, $right); } protected static function divide($left, $right): static { self::assertNumber($right); // anything divided by time is non-sensical return new self(parent::divide($left, $right); } protected static function mod($left, $right): static { throw new LogicException('Not implemented'); } protected static function exp($left, $right): static { throw new LogicException('Not implemented'); } protected static function comparable($left, $right): bool { if($left instanceof Time && $right instanceof Time) return true; return false; } public function asMinutes(): float { return $this / 60; } /* and so on */ }
This could provide valuable semantics, especially for usage in attributes:
define('SECOND', new Duration(1)); // use in attributes #[Delay(5*SECOND)] // use in properties public Duration $delay = 5 * SECOND;
While this isn't proper operator overrides, it does provide some much needed type safety when it comes to units.
Backward Incompatible Changes
There are no backward incompatible changes.
Proposed PHP Version(s)
8.4 if time allows, or the next version.
RFC Impact
To SAPIs
No impact.
To Existing Extensions
Only GMP will be affected.
To Opcache
There should be no impact to Opcache.
Open Issues
None, yet.
Unaffected PHP Functionality
Code using the GMP extension will be unaffected by this change as there will be no behavior changes.
Future Scope
- support for other operators
- serialization/unserialization
Proposed Voting Choices
- Allow extending the \GMP class and use a form of operator overloading
- Disallow extending the \GMP class
Note that voting “NO” to this RFC will mean we should make the \GMP class final so it cannot be extended any more, as it doesn't make sense to be able to extend it.
Patches and Tests
A prototype patch will be provided BEFORE this RFC goes to vote.
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
Links to external references, discussions or RFCs
Rejected Features
Keep this updated with features that were discussed on the mail lists.