rfc:compact-object-property-assignment

This is an old revision of the document!


PHP RFC: Compact Object Property Assignment

COPA: A pragmatic approach to object literals

  • Version: 1.0
  • Date: 2020-03-10
  • Author: Jakob Givoni jakob@givoni.dk
  • Status: Under Discussion

Introduction

Summary

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.

Example

Let’s start with an example that demonstrates the essence of COPA.

Instead of doing this...

$myObj->a = 1;
$myObj->b = 2;
$myObj->c = 3;

You will be able to do this:

$myObj->[
    a = 1,
    b = 2,
    c = 3,
];

And that’s all there is to it - the rest follow from this, as you’ll see in the use cases below.

Motivation

The purpose of this feature is to lighten the effort of populating data structures, especially medium to large ones.

Ideally the solution should meet the following criteria:

  • Brief - only mention the object once (less repetition)
  • Inline - object can be created and populated in a single expression (pseudo object literals, nested objects)
  • 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
  • Order-agnostic - properties can be specified in any order (though note that the order may change the result!)
  • 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

Proposal

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 = expression. A trailing comma , is permitted for the same reasons it's permitted in array literals and function calls (as of PHP 7.3). The whole block is considered an expression that returns the object we started with eg. $myObj.

Interpretation

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.

Use cases

DTOs - data transfer objects

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
class FooDto {
    public string $mane;
    public int $padme = 1; // Optional, with default
    public FooDto $hum;
}
 
$foo = new FooDto(); // Instantiating the object first
$foo->mane = 'get'; // Setting the properties
// Skipping the $padme property which has a default value
$foo->hum = new FooDto(); // Creating a nested DTO
$foo->hum->mane = 'life'; // Populating the nested DTO
 
doTheFoo($foo); // Passing it to a function
With new COPA syntax
doTheFoo((new FooDto())->[ // Constructing and populating inline
    mane = 'get',
    hum = (new FooDto())->[ // Even nested structures
        mane = 'life',
    ],
]);

Though the example is not a typical DTO, it represents the characteristics.

Argument bags

Argument bags are typically used when:

  • many arguments needs to be passed to a function
  • some arguments are optional
  • order of arguments is not important

With the proposed new syntax 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 current syntax
class Foo {
    protected string $mane;
    protected int $padme;
    protected string $hum;
 
    public function __construct(array $options) {
        $this->mane = $options['mane'];
        $this->padme = $options['padme'] ?? 1;
        $this->hum = $options['hum'];
    }
}
 
$myFoo = new Foo([
    'mane' => 'get', // Array syntax doesn't provide any help on parameter names
    'hum' => 'life', // or types
]);
With new COPA syntax
class FooOptions { // Separate concerns into an options class that handles optional and default values...
    public string $mane;
    public int $padme = 1; // Optional, with default
    public string $hum;
}
 
class Foo { // And the main class that receives the options and handles some feature
    protected FooOptions $options;
 
    public function __construct(FooOptions $options) {
        $this->options = $options;
    }
}
 
$myFoo = new Foo((new FooOptions())->[ // Objects as argument bags (pseudo named parameters?)
    mane = 'get', // Parameter name and type checking
    hum = 'life',
]);

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. RFC: Automatic Property Initialization, but which is probably also better left to the COPA argument bag example above.

Special cases

Clarification of edge-case behavior.

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:

$myObj->[
    foo = 10,
    bar = $myObj->foo + 20,
];
 
var_dump($myObj->bar); // int(30)

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.

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:

$p = 'foo';
$myObj->$p = 'bar'; // Variable property name
$a->{"fo" . "o"} = 'baz'; // Property name generated from expression
$a->b->c = 'hum'; // Creating default object from empty value
$a->d['e'] = 'dear'; // Setting array element inside property
$a->f++; // Increment/decrement of property value
 
$myObj->[
    $p = 'bar', // Syntax error
    {"foo"} = 'bar', // Syntax error
    b->c = 'hum', // Syntax error - but see Nested COPA below...
    d['e'] = 'dear', // Syntax
    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 a nested object in the same block, even if it has already been created:

// This example, using current syntax...
$foo->a = 1;
$foo->b->c = 2;
 
// Could be written with COPA like this:
$foo->[
    a = 1,
    b->[
        c = 2,
    ],
];

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:

Vanilla style population

class Foo {
    public int $bar;
    public int $baz;
}
 
$foo = new Foo();
$foo->bar = 1;
$foo->baz = 2;
 
doTheFoo($foo); // Cannot be done as an inline expression
 
// Oh yeah? What if I only need to set a single property?
doTheFoo((new Foo())->bar = 3); // Oops, fatal error: Can't use temporary expression in write context

Applying a touch of magic

/**
 * @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
    public function __call(string $method, array $params): self {
        if (strpos($method, 'get') === 0) {
            $name = substr($method, 3);
            $this->$name = current($params);
        }
        return $this;
    }
}
 
doTheFoo((new Foo()) // Works, but requires boilerplate code
    ->setBar(1)
    ->setBaz(2)
);

Anonymous classes have some tricks up their sleeves!

class Foo {
    public int $bar;
    public int $baz;
    public Foo $sub;
}
 
doTheFoo(new class extends Foo {
    public int $bar = 1; // Assigning values inline is now possible without creating setters!
    public int $baz = 2; // But I have to repeat their signature, which is annoying
 
    public function __construct() {
        // And if I need expressions, I have to use the constructor
        $this->sub =  new class extends Foo {
            public int $bar = 3;
            public int $baz = 4;
        };
   }
}); // Pretty ugly, I'm afraid...

Lambda expression

class Foo {
    public int $bar;
    public int $baz;
}
 
doTheFoo((function(){
   $foo = new Foo();
   $foo->bar = 1;
   $foo->baz = 2;
   return $foo;
})()); // Pretty good, if you can get those brackets straight... until you need to use values from the outside scope :-(

Anti-proposal

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:

COPA is NOT json

This is not a way to write object literals using JavaScript Object Notation (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.

COPA is NOT object initializer

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" (RFC: Object Initializer), giving you benefits very similar to object literals in a simple, pragmatic way.

COPA is NOT named parameters

Though on the wish list since 2013, named parameters (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 (RFC: Simplified Named Arguments) when you pass it to a function that expects it.

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.

Proposed PHP Version(s)

PHP 8.0

Open Issues

None so far. RFC still under discussion - any open questions coming up will be added here.

Proposed Voting Choices

The primary vote of whether or not to accept this RFC requires a 2/3 majority.

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.

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.

Implementation

After the project is implemented, this section should contain

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

References

rfc/compact-object-property-assignment.1584358991.txt.gz · Last modified: 2020/03/16 11:43 by jgivoni