rfc:compact-object-property-assignment
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
rfc:compact-object-property-assignment [2020/03/12 01:41] – jgivoni | rfc:compact-object-property-assignment [2020/03/22 03:17] – jgivoni | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Compact Object Property Assignment ====== | ====== PHP RFC: Compact Object Property Assignment ====== | ||
- | | + | |
- | * Date: 2020-03-10 | + | **COPA: A // |
+ | |||
+ | | ||
+ | * Date: 2020-03-17 | ||
* Author: Jakob Givoni < | * Author: Jakob Givoni < | ||
- | * Status: | + | * Status: |
===== Introduction ===== | ===== Introduction ===== | ||
==== Summary ==== | ==== Summary ==== | ||
- | **A pragmatic approach to mimicking object literals.** | ||
This RFC proposes a new, compact syntax to assign values to multiple properties on an object in a single expression. | This RFC proposes a new, compact syntax to assign values to multiple properties on an object in a single expression. | ||
+ | |||
+ | This //**pseudo object literal**// notation, (though not limited to such use) is intended to enable the developer to //**create an object and populating it inline**//, f.ex. directly as an argument in a function call. | ||
+ | |||
+ | As an alternative to writing a data structure as an associative array, COPA gives the data a **// | ||
+ | |||
+ | You may find that COPA is best suited when you don’t mind public properties, don’t require a constructor and is willing to compromise on magic enforcement of mandatory properties, since as [[https:// | ||
==== Example ==== | ==== Example ==== | ||
+ | |||
+ | Let’s start with an example that demonstrates the essence of COPA. | ||
=== Instead of doing this... === | === Instead of doing this... === | ||
+ | |||
<code php> | <code php> | ||
- | $myObj-> | + | $myObj = new Foo(); // 1. Create |
- | $myObj-> | + | |
- | $myObj-> | + | |
- | </code> | + | |
+ | $myObj-> | ||
+ | $myObj-> | ||
+ | $myObj-> | ||
+ | |||
+ | doTheFoo($myObj); | ||
+ | </ | ||
=== You will be able to do this: === | === You will be able to do this: === | ||
+ | |||
<code php> | <code php> | ||
- | $myObj->[ | + | doTheFoo((new Foo)->[ |
- | | + | |
- | | + | |
- | | + | |
- | ]; | + | ]); |
</ | </ | ||
+ | COPA is everything that comes after the object expression. You can use COPA on any expression that evaluates to an object. COPA is not tied to object construction, | ||
+ | |||
+ | //See more use cases below - and keep an open mind about the syntax; the options are limited these days :-)// | ||
==== Motivation ==== | ==== Motivation ==== | ||
- | The purpose of this feature is to lighten the effort of populating data structures, especially medium to large ones. | ||
- | Ideally | + | The purpose of this feature is to lighten |
+ | |||
+ | The goal was to find a solution | ||
* **Brief** - only mention the object once (less repetition) | * **Brief** - only mention the object once (less repetition) | ||
Line 39: | Line 58: | ||
* **Typo-proof** - property names can be autocompleted easily by IDE (faster typing, fewer errors) | * **Typo-proof** - property names can be autocompleted easily by IDE (faster typing, fewer errors) | ||
* **Type-checking** - IDE can verify correct type for typed properties and annotated virtual properties | * **Type-checking** - IDE can verify correct type for typed properties and annotated virtual properties | ||
- | * **Order-agnostic** - properties can be specified in any order (this doesn' | + | * **Order-agnostic** - properties can be specified in any order (though note that the order //may// change |
- | * **Sparcity** - there' | + | * **Sparcity** - any property |
* **Simple** - does what you would expect without introducing any new concepts into the language | * **Simple** - does what you would expect without introducing any new concepts into the language | ||
+ | |||
+ | //My focus is to find a **pragmatic** solution that can be implemented soon, but won’t trip up the continuing, inspiring development of the awesome language that is PHP.// | ||
+ | |||
+ | //If you have ever wanted to create, populate and send an object inside a function call, COPA is your chance!// | ||
===== Proposal ===== | ===== Proposal ===== | ||
==== Syntax ==== | ==== Syntax ==== | ||
- | The proposed syntax | + | |
- | A trailing comma is permitted for the same reasons it's permitted in array literals and function calls (as of PHP 7.3). | + | The proposed syntax |
- | The whole block is considered an expression that returns the object we started | + | |
+ | < | ||
+ | < | ||
+ | `< | ||
+ | [repeat as needed], | ||
+ | ] | ||
+ | </ | ||
+ | A trailing comma is permitted for the same reasons it's permitted in array literals and [[https:// | ||
+ | |||
+ | The whole block is considered an expression that returns the object | ||
+ | |||
+ | This syntax was chosen for its availability in the language. If we land on another syntax, I’m not married to this one. The only criteria are that it doesn’t conflict | ||
==== Interpretation ==== | ==== Interpretation ==== | ||
- | Each comma-separated assignment inside the curly brackets is executed as an assignment of the named property on the object preceding the block. If the property is defined and publicly accessible, it will simply be set, or possible throw a TypeError. If there' | ||
- | When used in an expression, COPA simply returns the object itself. | ||
+ | Each comma-separated assignment inside the brackets is executed as an assignment of the named property on the object preceding the block. If the property is defined and publicly accessible, it will simply be set, or possible throw a '' | ||
+ | |||
+ | If you replace COPA with single line assignments, | ||
+ | |||
+ | <code php> | ||
+ | $foo->[ | ||
+ | a = 1, | ||
+ | b = myfunc(), | ||
+ | c = $foo-> | ||
+ | ]; | ||
+ | |||
+ | // The COPA above is identical to | ||
+ | $foo->a = 1; | ||
+ | $foo->b = myfunc(); | ||
+ | $foo->c = $foo-> | ||
+ | </ | ||
==== Use cases ==== | ==== Use cases ==== | ||
+ | |||
+ | === Alternative to passive associative arrays === | ||
+ | |||
+ | <code php> | ||
+ | // Instead of this: | ||
+ | |||
+ | doSomething([ | ||
+ | ' | ||
+ | ' | ||
+ | ]); | ||
+ | |||
+ | // Use COPA: | ||
+ | |||
+ | class Options { | ||
+ | public $a; | ||
+ | public $b; | ||
+ | } | ||
+ | |||
+ | doSomething((new Options)-> | ||
+ | a = 1, // Parameter name and type checking | ||
+ | b = 2, | ||
+ | ]); | ||
+ | </ | ||
+ | //If you often create, populate and send the same families of data structure, declaring those structures and using COPA makes it a breeze.// | ||
=== DTOs - data transfer objects === | === DTOs - data transfer objects === | ||
- | * many properties | ||
- | * some properties may be optional or have default values | ||
- | * properties may be public so we may avoid writing a lot of boilerplate code when defining them | ||
- | With COPA we can create the whole object inline right inside the function call: | + | Typical characteristics of DTOs: |
+ | |||
+ | * many properties | ||
+ | * properties may be optional, with default values | ||
+ | * public visibility on properties, i.e. no desire for boilerplate code to create setters and getters for each one | ||
+ | * desirability to create, populate and send in one go | ||
== With current syntax == | == With current syntax == | ||
+ | |||
<code php> | <code php> | ||
- | class Dto { | + | class FooDto |
- | public string $foo; | + | public |
- | public int $bar = 1; // Optional, with default | + | public int $padme = 1; // Optional, with default |
- | public | + | public |
} | } | ||
- | $myDto = new Dto(); // Instantiating the object first | + | $foo = new FooDto(); // Instantiating the object first |
- | $myDto->foo = ' | + | $foo->mane = ' |
- | $myDto->baz = ' | + | // Skipping the $padme property which has a default value |
+ | $foo->hum = new FooDto(); // Creating a nested DTO | ||
+ | $foo-> | ||
- | myFunc($myDto); // Passing to a function | + | doTheFoo($foo); // Passing |
</ | </ | ||
+ | == With new COPA syntax == | ||
- | == With new syntax == | ||
<code php> | <code php> | ||
- | myFync((new Dto())->[ // Constructing and populating inline | + | doTheFoo((new FooDto)->[ // Constructing and populating inline |
- | | + | |
- | | + | |
+ | mane = ' | ||
+ | ], | ||
]); | ]); | ||
</ | </ | ||
+ | //Though the example is not a typical DTO, it represents the characteristics.// | ||
=== Argument bags === | === Argument bags === | ||
- | | + | |
+ | Argument bags are typically used when: | ||
+ | |||
+ | | ||
* some arguments are optional | * some arguments are optional | ||
* order of arguments is not important | * order of arguments is not important | ||
- | + | ||
- | With COPA we can avoid using simple arrays and instead get autocomplete and type-checking in the IDE with a syntax that smells of named parameters: | + | With the proposed new syntax |
== With current syntax == | == With current syntax == | ||
+ | |||
<code php> | <code php> | ||
class Foo { | class Foo { | ||
- | protected string $foo; | + | protected string $mane; |
- | protected int $bar; | + | protected int $padme; |
- | protected string $baz; | + | protected string $hum; |
public function __construct(array $options) { | public function __construct(array $options) { | ||
- | $this->foo = $options[' | + | $this->mane = $options[' |
- | $this->bar = $options[' | + | $this->padme = $options[' |
- | $this->baz = $options[' | + | $this->hum = $options[' |
} | } | ||
} | } | ||
$myFoo = new Foo([ | $myFoo = new Foo([ | ||
- | 'foo' => ' | + | 'mane' => ' |
- | 'baz' => ' | + | 'hum' => ' |
]); | ]); | ||
</ | </ | ||
+ | == With new COPA syntax == | ||
- | == Alternatively, | ||
- | <code php> | ||
- | class Foo { | ||
- | protected string $foo; | ||
- | protected int $bar; | ||
- | protected string $baz; | ||
- | |||
- | public function __construct(string $foo, int $bar, string $baz) { | ||
- | $this-> | ||
- | $this-> | ||
- | $this-> | ||
- | } | ||
- | } | ||
- | |||
- | $myFoo = new Foo(' | ||
- | </ | ||
- | |||
- | == With new syntax == | ||
<code php> | <code php> | ||
class FooOptions { // Separate concerns into an options class that handles optional and default values... | class FooOptions { // Separate concerns into an options class that handles optional and default values... | ||
- | public string $foo; | + | public |
- | public int $bar = 1; // Optional, with default | + | public int $padme = 1; // Optional, with default |
- | public string $baz; | + | public |
} | } | ||
Line 142: | Line 209: | ||
public function __construct(FooOptions $options) { | public function __construct(FooOptions $options) { | ||
+ | // Do some validate here if you must, f.ex. checking for mandatory parameters | ||
$this-> | $this-> | ||
} | } | ||
} | } | ||
- | $myFoo = new Foo((new FooOptions())->[ | + | $myFoo = new Foo((new FooOptions)-> |
- | | + | |
- | | + | |
]); | ]); | ||
</ | </ | ||
+ | //The other alternative to an argument bag is usually a constructor with many arguments, which is something that has been attempted to solve with RFCs arguing for automatic promotion of arguments to properties (f.ex. [[https:// | ||
==== Special cases ==== | ==== Special cases ==== | ||
+ | |||
+ | Clarification of edge-case behavior. | ||
=== Execution order === | === Execution order === | ||
- | The fact that the assignments are executed in the order they are listed, just as if they had been specified on separate lines, has the following consequence: | + | |
+ | The fact that the assignments are executed in the order they are listed | ||
<code php> | <code php> | ||
$myObj-> | $myObj-> | ||
Line 164: | Line 237: | ||
var_dump($myObj-> | var_dump($myObj-> | ||
</ | </ | ||
+ | //As the assignments are carried out in order on the object, you can use the new value of a previous assigment in a following one.// | ||
//There may be arguments equally for and against this behavior, but ultimately the simplicity of the design and implementation wins, in my opinion.// | //There may be arguments equally for and against this behavior, but ultimately the simplicity of the design and implementation wins, in my opinion.// | ||
- | === Expressions in property names === | + | === Exceptions |
- | Property names must be expressed literally. Some examples | + | |
+ | If an expression inside a COPA block throws an exception, the result is the same as if the assignments had been done the old way, f.ex. if we have: | ||
+ | |||
+ | <code php> | ||
+ | class Foo { | ||
+ | public $a; | ||
+ | public $b; | ||
+ | public $c; | ||
+ | } | ||
+ | |||
+ | $foo = new Foo(); | ||
+ | |||
+ | function iThrow() { | ||
+ | throw new \Exception(); | ||
+ | } | ||
+ | </ | ||
+ | Then the following | ||
+ | |||
+ | <code php> | ||
+ | // With COPA | ||
+ | try { | ||
+ | $foo-> | ||
+ | a = 'a', | ||
+ | b = iThrow(), | ||
+ | c = ' | ||
+ | ]; | ||
+ | } catch (\Throwable $e) { | ||
+ | var_dump($foo); | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | <code php> | ||
+ | // Without COPA | ||
+ | try { | ||
+ | $foo-> | ||
+ | -> | ||
+ | -> | ||
+ | } catch (\Throwable $e) { | ||
+ | var_dump($foo); | ||
+ | } | ||
+ | |||
+ | // OR | ||
+ | |||
+ | try { | ||
+ | $foo->a = ' | ||
+ | $foo->b = iThrow(); | ||
+ | $foo->c = ' | ||
+ | } catch (\Throwable $e) { | ||
+ | var_dump($foo); | ||
+ | } | ||
+ | </ | ||
+ | The result | ||
+ | |||
+ | <code php> | ||
+ | object(Foo)# | ||
+ | [" | ||
+ | string(1) " | ||
+ | [" | ||
+ | NULL | ||
+ | [" | ||
+ | NULL | ||
+ | } | ||
+ | </ | ||
+ | //I.e. COPA is not an atomic operation in the same way method chaining isn’t.// | ||
+ | |||
+ | ==== Out of scope / future scope ==== | ||
+ | |||
+ | This section contains a tentative list of features that may not be implemented. | ||
+ | |||
+ | === Can you do that? === | ||
+ | |||
+ | The following examples show some things that is now possible using regular property accessor, but which will not also be supported with COPA: | ||
<code php> | <code php> | ||
$p = ' | $p = ' | ||
$myObj-> | $myObj-> | ||
- | $a-> | + | $a-> |
- | + | $a-> | |
+ | $a-> | ||
+ | $a->f++; // Increment/ | ||
$myObj-> | $myObj-> | ||
$p = ' | $p = ' | ||
{" | {" | ||
+ | b->c = ' | ||
+ | d[' | ||
+ | f++, // Syntax error | ||
]; | ]; | ||
</ | </ | ||
+ | //If anyone can show that any these features would be significantly desirable and simultaneously rather trivial to implement, let’s discuss.// | ||
- | // | + | === Nested COPA === |
+ | It might be nice to be able to populate an existing nested object in the same block: | ||
+ | |||
+ | <code php> | ||
+ | // This example, using current syntax... | ||
+ | $foo->a = 1; | ||
+ | $foo-> | ||
+ | |||
+ | // Could be written with COPA like this: | ||
+ | $foo->[ | ||
+ | a = 1, | ||
+ | b->[ | ||
+ | c = 2, | ||
+ | ], | ||
+ | ]; | ||
+ | |||
+ | // But for now you'll have to do this: | ||
+ | $foo->[ | ||
+ | a = 1, | ||
+ | b = $foo-> | ||
+ | c = 2, | ||
+ | ], | ||
+ | ]; | ||
+ | |||
+ | </ | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
- | None. Array followed by square bracket causes syntax error in PHP 7.4. | + | |
- | This new syntax is optional. If you don't use it, your code will continue to run. | + | None. Array followed by square bracket causes syntax error in PHP 7.4. This new syntax is optional. If you don't use it, your code will continue to run. |
===== Proposed PHP Version(s) ===== | ===== Proposed PHP Version(s) ===== | ||
- | Next PHP 8.x | + | |
+ | PHP 8.0 | ||
===== Open Issues ===== | ===== Open Issues ===== | ||
+ | |||
+ | ==== Alternative syntaxes ==== | ||
+ | |||
+ | I’m going to suggest some alternative syntaxes, that we can vote on, provided their feasibility has been vetted by an experienced internals developer: | ||
+ | |||
+ | === Syntax A === | ||
+ | |||
+ | This is the originally proposed one: | ||
+ | |||
+ | <code php> | ||
+ | $foo->[ | ||
+ | a = 1, | ||
+ | b = 2, | ||
+ | c = (new Foo)->[ | ||
+ | a = 3, | ||
+ | b = 4, | ||
+ | ], | ||
+ | ]; | ||
+ | </ | ||
+ | === Syntax B === | ||
+ | |||
+ | Since the [[https:// | ||
+ | |||
+ | <code php> | ||
+ | $foo{ | ||
+ | a = 1, | ||
+ | b = 2, | ||
+ | c = (new Foo){ | ||
+ | a = 3, | ||
+ | b = 4, | ||
+ | }, | ||
+ | }; | ||
+ | </ | ||
+ | === Syntax C === | ||
+ | |||
+ | No wrapper: | ||
+ | |||
+ | <code php> | ||
+ | $foo-> | ||
+ | a = 1, | ||
+ | b = 2, | ||
+ | c = (new Foo)-> | ||
+ | a = 3, | ||
+ | b = 4, | ||
+ | ;, | ||
+ | ; | ||
+ | </ | ||
+ | Nesting becomes awkward - how do we jump out again? | ||
+ | |||
+ | === Syntax D === | ||
+ | |||
+ | Repeating the array for familiarity: | ||
+ | |||
+ | <code php> | ||
+ | $foo | ||
+ | ->a = 1, | ||
+ | ->b = 2, | ||
+ | ->c = (new Foo) | ||
+ | ->a = 3, | ||
+ | ->b = 4, | ||
+ | ;, | ||
+ | ; | ||
+ | </ | ||
+ | Same issue with nested as previous. We need to find a way to express end of COPA block. | ||
+ | |||
+ | === Syntax E === | ||
+ | |||
+ | Like the original but with normal brackets instead of square ones: | ||
+ | |||
+ | <code php> | ||
+ | $foo->( | ||
+ | a = 1, | ||
+ | b = 2, | ||
+ | c = (new Foo)->( | ||
+ | a = 3, | ||
+ | b = 4, | ||
+ | ), | ||
+ | ); | ||
+ | </ | ||
+ | ==== Mandatory properties ==== | ||
+ | |||
+ | Some criticism has been that COPA is of little use without also enforcing mandatory properties to be set. | ||
+ | |||
+ | Rowan Tommins: | ||
+ | |||
+ | > It seems pretty rare that an object would have no mandatory properties, so saying “if you have a mandatory property, COPA is not for you” is ruling out a lot of uses. | ||
+ | |||
+ | Michał Brzuchalski: | ||
+ | |||
+ | > This helps to avoid bugs where a property is added to the class but forgot to be assigned it a value in all cases where the class is instantiated and initialized | ||
+ | |||
+ | Mandatory properties are typed properties without a default value. They are in the uninitialized state until they are assigned a value. It has been suggested that an exception should be thrown at the end of the constructor if any property is still uninitialized, | ||
+ | |||
+ | For now you must continue to write your own validation code to be carried out at the appropriate “point of no return”. | ||
+ | |||
+ | ==== Atomic operations ==== | ||
+ | |||
+ | It’s also been suggested that assigning multiple values using COPA should be an atomic operation that either succeeds or fails in its entirety. | ||
+ | |||
+ | That does sound cool as well, and may seem like the expected behavior for some. | ||
+ | |||
+ | Still, I’m not convinved it’s worth the extra hassle, since what were you planning to do with the incomplete object anyway? | ||
+ | |||
+ | Chaining method calls are not an atomic operation and if an exception is thrown in the middle I doubt you would raise an eyebrow about the previous call having altered the state of the object. | ||
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
+ | |||
+ | The primary vote of whether or not to accept this RFC requires a 2/3 majority. | ||
+ | |||
+ | A secondary “vote” directed at no-voters, will ask you the primary reason for voting “No”. | ||
+ | |||
+ | The options will be: | ||
+ | |||
+ | - I voted yes! | ||
+ | - I don’t find the feature useful | ||
+ | - I don’t like the syntax | ||
+ | - I prefer a more comprehensive solution to this problem | ||
+ | - I prefer a narrower solution to this problem | ||
+ | - This breaks backwards compatibility | ||
+ | - This will negatively limit future changes | ||
+ | - This will be a nightmare to implement and maintain | ||
+ | - I prefer not to say | ||
+ | |||
+ | This will help understand what the obstacles are, when studying this RFC in the future, should anyone be tempted to have another shot at object literals et. al. | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
+ | |||
There are yet no patches nor tests. The question of who will be developing this will be addressed if the RFC passes. | There are yet no patches nor tests. The question of who will be developing this will be addressed if the RFC passes. | ||
===== Implementation ===== | ===== Implementation ===== | ||
- | After the project is implemented, | + | |
+ | After the project is implemented, | ||
- the version(s) it was merged into | - the version(s) it was merged into | ||
- a link to the git commit(s) | - a link to the git commit(s) | ||
Line 205: | Line 507: | ||
===== References ===== | ===== References ===== | ||
+ | |||
+ | Related RFCs: | ||
+ | |||
* https:// | * https:// | ||
* https:// | * https:// | ||
* https:// | * https:// | ||
* https:// | * https:// | ||
- | + | * https:// | |
- | ===== Rejected Features ===== | + | * https:// |
+ | * https:// | ||
+ | * https:// | ||
rfc/compact-object-property-assignment.txt · Last modified: 2020/04/14 06:30 by jgivoni