rfc:compact-object-property-assignment

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Next revisionBoth sides next revision
rfc:compact-object-property-assignment [2020/03/17 02:53] jgivonirfc: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 ======
  
-**COPA: A pragmatic approach to object literals**+**COPA: A //pragmatic// approach to object literals**
  
-  * Version: 1.1 +  * Version: 1.3 
-  * Date: 2020-03-16+  * Date: 2020-03-17
   * Author: Jakob Givoni <jakob@givoni.dk>   * Author: Jakob Givoni <jakob@givoni.dk>
-  * Status: Under Discussion +  * Status: Under [[https://externals.io/message/109055|Discussion]]
-  * Discussion: https:%%//%%externals.io/message/109055+
  
 ===== Introduction ===== ===== Introduction =====
Line 15: Line 14:
 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, similar to what is possible for arrays.+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 **//documented signature//** so that you know what parameters are expected and their values. 
 + 
 +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://wiki.php.net/rfc/write_once_properties#read-only_semantics|the saying goes]]: //Object construction is a fuzzy concept in PHP and lazy initialisation is a feature!//
  
 ==== Example ==== ==== Example ====
Line 24: Line 27:
  
 <code php> <code php>
-$myObj->a = 1;+$myObj = new Foo(); // 1. Create 
 + 
 +$myObj->a = 1; // 2. Populate
 $myObj->b = 2; $myObj->b = 2;
 $myObj->c = 3; $myObj->c = 3;
 +
 +doTheFoo($myObj); // 3. Send
 </code> </code>
 === You will be able to do this: === === You will be able to do this: ===
  
 <code php> <code php>
-$myObj->[+doTheFoo((new Foo)->[
     a = 1,     a = 1,
     b = 2,     b = 2,
     c = 3,     c = 3,
-];+]);
 </code> </code>
-And that’s all there is to it - the rest follow from this, as you’ll see in the use cases below.+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, but can be used anytime, anywhere in the objects lifeas many times as you want. 
 + 
 +//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.+The purpose of this feature is to lighten the effort of creating, populating and sending data structures, from small to large ones.
  
-Ideally the solution should meet the following criteria:+The goal was to find a solution that meets the following criteria:
  
   * **Brief** - only mention the object once (less repetition)   * **Brief** - only mention the object once (less repetition)
Line 52: Line 61:
   * **Sparcity** - any property can be “skipped” (“skipped” properties may acquire a default value)   * **Sparcity** - any property can be “skipped” (“skipped” properties may acquire a default value)
   * **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 =====
Line 57: Line 70:
 ==== Syntax ==== ==== Syntax ====
  
-The proposed syntax following an object expression ''%%$myObj%%'' | ''%%(new MyClass())%%'' is the object arrow operator ''%%->%%'' followed by a set of square brackets ''%%[…]%%'' containing a comma-separated list of property name equals ''%%=%%'' expressionA trailing comma ''%%,%%'' is permitted for the same reasons it's permitted in array literals and [[https://wiki.php.net/rfc/trailing-comma-function-calls|function calls (as of PHP 7.3)]]. The whole block is considered an expression that returns the object we started with ''%%eg$myObj%%''.+The proposed syntax consists of the object arrow operator followed by a set of square brackets containing a comma-separated list of assignments in the form of property name equals expression: 
 + 
 +<code> 
 +<object-expression> -> [ 
 +    `<property-name> <expression>`, 
 +    [repeat as needed], 
 +
 +</code> 
 +A trailing comma is permitted for the same reasons it's permitted in array literals and [[https://wiki.php.net/rfc/trailing-comma-function-calls|function calls (as of PHP 7.3)]]. 
 + 
 +The whole block is considered an expression that returns the object before the arrow. 
 + 
 +This syntax was chosen for its availability in the language. If we land on another syntax, I’m not married to this oneThe only criteria are that it doesn’t conflict with anything else, that it is brief and feels good.
  
 ==== Interpretation ==== ==== Interpretation ====
Line 63: Line 88:
 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 ''%%TypeError%%''. If there's no property with that name, or if it's protected or private, the magic method ''%%__set%%'' will be called just like you would expect. 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 ''%%TypeError%%''. If there's no property with that name, or if it's protected or private, the magic method ''%%__set%%'' will be called just like you would expect. When used in an expression, **COPA** simply returns the object itself.
  
 +If you replace COPA with single line assignments, you will always get the same result, f.ex.:
 +
 +<code php>
 +$foo->[
 +    a = 1,
 +    b = myfunc(),
 +    c = $foo->bar(),
 +];
 +
 +// The COPA above is identical to
 +$foo->a = 1;
 +$foo->b = myfunc();
 +$foo->c = $foo->bar();
 +</code>
 ==== Use cases ==== ==== Use cases ====
 +
 +=== Alternative to passive associative arrays ===
 +
 +<code php>
 +// Instead of this:
 +
 +doSomething([
 +    'a' => 1, // Array syntax doesn't provide any help on parameter names
 +    'b' => 2, // or types
 +]);
 +
 +// Use COPA:
 +
 +class Options {
 +    public $a;
 +    public $b;
 +}
 +
 +doSomething((new Options)->[
 +    a = 1, // Parameter name and type checking
 +    b = 2,
 +]);
 +</code>
 +//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 ===
Line 78: Line 141:
 <code php> <code php>
 class FooDto { class FooDto {
-    public string $mane;+    public ?string $mane = null;
     public int $padme = 1; // Optional, with default     public int $padme = 1; // Optional, with default
-    public FooDto $hum;+    public ?FooDto $hum = null;
 } }
  
Line 94: Line 157:
  
 <code php> <code php>
-doTheFoo((new FooDto())->[ // Constructing and populating inline+doTheFoo((new FooDto)->[ // Constructing and populating inline
     mane = 'get',     mane = 'get',
-    hum = (new FooDto())->[ // Even nested structures+    hum = (new FooDto)->[ // Even nested structures
         mane = 'life',         mane = 'life',
     ],     ],
Line 137: Line 200:
 <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 $mane;+    public ?string $mane = null;
     public int $padme = 1; // Optional, with default     public int $padme = 1; // Optional, with default
-    public string $hum;+    public ?string $hum = null;
 } }
  
Line 146: 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->options = $options;         $this->options = $options;
     }     }
 } }
  
-$myFoo = new Foo((new FooOptions())->[ // Objects as argument bags (pseudo named parameters?)+$myFoo = new Foo((new FooOptions)->[ // Objects as argument bags (pseudo named parameters?)
     mane = 'get', // Parameter name and type checking     mane = 'get', // Parameter name and type checking
     hum = 'life',     hum = 'life',
Line 194: Line 258:
 } }
 </code> </code>
-Then the following two blocks behave identically:+Then the following examples behave identically:
  
 <code php> <code php>
Line 211: Line 275:
 <code php> <code php>
 // Without COPA // Without COPA
 +try {
 +    $foo->setA('a')
 +        ->setB(iThrow())
 +        ->setC('c');
 +} catch (\Throwable $e) {
 +    var_dump($foo);
 +}
 +
 +// OR
 +
 try { try {
     $foo->a = 'a';     $foo->a = 'a';
Line 219: Line 293:
 } }
 </code> </code>
-Result, ''%%a%%'' will be set, ''%%b%%'' and ''%%c%%'' will not:+The result in all cases is that ''%%a%%'' will be set, while ''%%b%%'' and ''%%c%%'' will not:
  
 <code php> <code php>
Line 231: Line 305:
 } }
 </code> </code>
 +//I.e. COPA is not an atomic operation in the same way method chaining isn’t.//
 +
 ==== Out of scope / future scope ==== ==== Out of scope / future scope ====
  
Line 259: Line 335:
 === Nested COPA === === Nested COPA ===
  
-It might be nice to be able to populate nested object in the same block, even if it has already been created:+It might be nice to be able to populate an existing nested object in the same block:
  
 <code php> <code php>
Line 273: Line 349:
     ],     ],
 ]; ];
-</code> 
-===== Why don't you just... ===== 
  
-What follows is a handful of alternative ways to populate an object with existing syntax, and some hints as to why they just doesn't cut it:+// But for now you'll have to do this: 
 +$foo->
 +    a = 1, 
 +    b = $foo->b->
 +        c = 2, 
 +    ], 
 +];
  
-==== Vanilla style population ====+</code> 
 +===== Backward Incompatible Changes =====
  
-<code php> +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.
-class Foo { +
-    public int $bar; +
-    public int $baz; +
-}+
  
-$foo new Foo()+===== Proposed PHP Version(s) =====
-$foo->bar 1; +
-$foo->baz 2;+
  
-doTheFoo($foo); // Cannot be done as an inline expression+PHP 8.0
  
-// Oh yeah? What if I only need to set a single property? +===== Open Issues =====
-doTheFoo((new Foo())->bar 3); // Oops, fatal error: Can't use temporary expression in write context +
-</code> +
-==== Applying a touch of magic ====+
  
-<code php> +==== Alternative syntaxes ====
-/** +
- * @method self setBar(int $bar) // Use annotations +
- * @method self setBaz(int $baz) // Duplicate the property signatures +
- */ +
-class Foo { +
-    protected int $bar; +
-    protected int $baz;+
  
-    // This generic method could be injected using a trait +I’m going to suggest some alternative syntaxesthat we can vote on, provided their feasibility has been vetted by an experienced internals developer:
-    public function __call(string $methodarray $params)self { +
-        if (strpos($method, 'get') === 0) { +
-            $name = substr($method, 3); +
-            $this->$name = current($params); +
-        } +
-        return $this; +
-    } +
-}+
  
-doTheFoo((new Foo())  +=== Syntax A ===
-    ->setBar(1) +
-    ->setBaz(2) +
-); +
-</code> +
-//Works, but requires some boilerplate code//+
  
-==== Anonymous classes have some tricks up their sleeves! ====+This is the originally proposed one:
  
 <code php> <code php>
-class Foo { +$foo->[ 
-    public int $bar; +    a = 1, 
-    public int $baz; +    b = 2, 
-    public Foo $sub+    c = (new Foo)->[ 
-}+        a = 3, 
 +        b = 4, 
 +    ], 
 +]
 +</code> 
 +=== Syntax B ===
  
-doTheFoo(new class extends Foo { +Since the [[https://wiki.php.net/rfc/deprecate_curly_braces_array_access|deprecation of curly brackets as array access in PHP 7.4]]that notation could be used to assign properties:
-    public int $bar = 1; // Assigning values inline is now possible without creating setters! +
-    public int $baz = 2; // But I have to repeat their signaturewhich is annoying+
  
-    public function __construct() +<code php> 
-        // And if I need expressionsI have to use the constructor +$foo
-        $this->sub  new class extends Foo { +    a = 1
-            public int $bar = 3; +    2, 
-            public int $baz = 4; +    c = (new Foo)
-        }; +        = 3, 
-   } +        = 4, 
-});+    }, 
 +};
 </code> </code>
-//Pretty ugly, I’m afraid…//+=== Syntax C ===
  
-==== Lambda expression ====+No wrapper:
  
 <code php> <code php>
-class Foo { +$foo-> 
-    public int $bar; +    a = 1, 
-    public int $baz; +    b = 2, 
-+    c = (new Foo)-> 
- +        a 3, 
-doTheFoo((function(){ +        4, 
-   $foo = new Foo()+    ;, 
-   $foo->bar 1; +;
-   $foo->baz 2; +
-   return $foo+
-})())+
 </code> </code>
-//Pretty good, if you can get those brackets straight… until you need to use values from the outside scope :-(//+Nesting becomes awkward how do we jump out again?
  
-===== Anti-proposal =====+=== Syntax D ===
  
-This proposal is related to previous RFCs and shares motivation with them. However, though **COPA** claims to be in the same family, here are some disclaimers:+Repeating the array for familiarity:
  
-==== COPA is NOT json ====+<code php> 
 +$foo 
 +    ->1, 
 +    ->2, 
 +    ->(new Foo) 
 +        ->3, 
 +        ->b = 4, 
 +    ;, 
 +
 +</code> 
 +Same issue with nested as previous. We need to find a way to express end of COPA block.
  
-This is not a way to write object literals using JavaScript Object Notation [[https://wiki.php.net/rfc/objectarrayliterals|(RFC: First-Class Object and Array Literals)]]. It's similar to an array literal, but with each key actually corresponding to a defined property of the object. We don't want to quote the property names as there's no advantage, only added overhead. The equals sign is used straightforwardly to denote assignment. Square brackets have been chosen instead of curly ones because the latter already has an interpretation when following the object arrow, namely to create an expression which will return a property or method name.+=== Syntax E ===
  
-==== COPA is NOT object initializer ====+Like the original but with normal brackets instead of square ones:
  
-You could call it **//pseudo// object literal** notation because we're not dictating the //actual// inner state of the object, we're merely populating properties after construction. But this //does// allow for a ''%%"literal syntax for creating an object and initializing properties"%%'' [[https://wiki.php.net/rfc/object-initializer|(RFC: Object Initializer)]], giving you benefits very similar to object literals in simplepragmatic way.+<code php
 +$foo->( 
 +    a = 1, 
 +    b = 2, 
 +    c = (new Foo)->( 
 +        = 3, 
 +        b = 4, 
 +    ), 
 +); 
 +</code> 
 +==== Mandatory properties ====
  
-==== COPA is NOT named parameters ====+Some criticism has been that COPA is of little use without also enforcing mandatory properties to be set.
  
-Though on the wish list since 2013, named parameters [[https://wiki.php.net/rfc/named_params|(RFC: Named Parameters)]] have proven to be a tough nut to crack. But with this RFC you will be able to create parameter objects that may give you benefits very similar to named parameters [[https://wiki.php.net/rfc/simplified_named_params|(RFC: Simplified Named Arguments)]] when you pass it to a function that expects it.+Rowan Tommins:
  
-===== Backward Incompatible Changes =====+> 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.
  
-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.+Michał Brzuchalski:
  
-===== Proposed PHP Version(s) =====+> 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
  
-PHP 8.0+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, but this idea has not yet caught on. COPA doesn’t have any obvious way of enforcing mandatory properties.
  
-===== Open Issues =====+For now you must continue to write your own validation code to be carried out at the appropriate “point of no return”.
  
-==== Alternative syntaxes ====+==== Atomic operations ====
  
-Some alternative syntaxes for COPA has been suggested, but I’m not convinced they can be implemented without hassle:+It’s also been suggested that assigning multiple values using COPA should be an atomic operation that either succeeds or fails in its entirety.
  
-<code php> +That does sound cool as welland may seem like the expected behavior for some.
-$foo = new Foo()[ +
-   property1 = "hello", +
-   property2 = 5, +
- ]; +
-</code> +
-For some reason it’s not possible to do this:+
  
-<code php> +StillI’m not convinved it’s worth the extra hassle, since what were you planning to do with the incomplete object anyway?
-new Foo()->doSomething(); // syntax errorunexpected '->' +
-</code> +
-It’s necessary to wrap the instantiation in brackets:+
  
-<code php> +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.
-(new Foo())->doSomething(); // Ok +
-</code> +
-Which is why I think it will be necessary in my proposal as well.+
  
-Furthermore, a variable or object directly followed by square brackets usually imply array access on it. That syntax would conflict with COPA.+===== 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”.
  
-//Unless someone can convince me that it’s trivial to implement another syntax that looks even better, my stance is that the people who are going to vote no on this because they don’t find the feature useful are not gonna change their mind if the syntax changes, and the people who find this feature useful will prefer rapid adaptation over solving implementation issues.//+The options will be:
  
-===== Proposed Voting Choices ===== +  - I voted yes! 
- +  - I don’t find the feature useful 
-The primary vote of whether or not to accept this RFC requires 2/3 majority.+  - I don’t like the syntax 
 +  - I prefer a more comprehensive solution to this problem 
 +  - I prefer 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
  
-There may be a secondary “vote” directed at no-voters, where you’ll be asked the primary reason for voting “No”. 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.+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 =====
rfc/compact-object-property-assignment.txt · Last modified: 2020/04/14 06:30 by jgivoni