> On Sep 13, 2019, at 3:18 AM, Rasmus Schultz <ras...@mindplay.dk 
> <mailto:ras...@mindplay.dk>> wrote:
> 

> All in all, I find this feature is useful or applicable only to a few,
> select patterns within the language - it isn't general enough.
> ...
> In my opinion, language features should be as general as possible
> ...
> My strong preference over this feature would be named parameters, 
> ...
> It works for models with private/protected fields, classes with accessors,
> classes with validations in constructors or factory-methods, and so on.
> 
> In other words, it works more generally with all common patterns and
> practices - in many ways, it just seems like a better fit for the language.


I mostly agree with the first statements, in abstract principle.  

But ironically I come to the exact opposite conclusion that your latter 
statements imply, i.e. that named parameters are not a generalizable enough 
feature and that object initializers are much more generalizable.

Let me explain where I see limitations of named parameters:

1. Nested structures: Named parameters, assuming they are not shorthand for 
initializing objects could not support nested parameters, or at least not 
without a lot of other supporting additions to PHP too.

$car = new Car({ 
   yearOfProduction => 1975, 
   vin => "12345678", 
   engine => {
      type => "v8", 
      capacity => "270ci", 
   },
})

2. Local usage: Consider the following construct. I might use this above the 
top of a loop if I were writing a very add-hoc custom report.  (Please note 
that I embellished by adding types in a manner I would prefer here.) 

The following example shows anonymous class declared in local scope.  The 
reason we want to initialize a anonymous class is the same reason we wanted 
anonymous classes in the first place; often a class is just not important 
enough to assign a name. 

And since naming is "one of the 2 or 3 hardest things in programming" not 
having to assign it a name if a name is not needed is a real plus. And this is 
not a use-case named parameters address, so this is an explicit example of 
where object initializers are more general purpose than named parameters:

$stats = new class {
    int total   => 0,
    int mean    => 0,
    int average => 0,
    int median  => 0,
}

I won't elaborate on how this class is used since hopefully that code would be 
obvious . 

But I will say that I would use these types of object initializers almost 
everywhere I previously have used arrays — and where others have used arrays in 
code I am refactoring.  That would reduce most of array use-cases to 
collections as I would no longer really need them as associative arrays, unless 
and framework or library required me to use them. 

If we had the language features to allow us to concisely move locally-declared 
associative array code to using anonymous class object instances then IDEs 
would be able to validate proper use of class properties and cut down on an 
entire class of coding errors.  

(Yes, theoretically we can already use anonymous classes instead of arrays but 
declaring the classes and initializing their properties is so tedious that 
almost nobody ever codes that way in PHP. Or at least none of the code I have 
ever come across on GitHub or in client's projects.)

3. Passing to subroutines.  This is a contrived example, but it is emblematic 
of code I write daily. 

The unfortunate aspect is that since I use an array of $args I cannot have my 
IDE nor PHP validate that I used the proper array keys.

(Note that my example has an error; I used 'field' with  query() instead of 
'fields' as I should have, to illustrate the problem):

class Join {
   public string $table;
   public string $from;
   public string $to;
}

class QueryBuilder {
   function query(array $args):object[] {
      if ( ! $this->validate($args) ) {
         throw new Exception("Dude!");
      }
      $query = $this->build($args);
      return $this->run($query);
   }
   function validate(array $args) {
      ...
   }
   function build(array $args) {
      ...
   }
   function run(string $query ) {
      ...
   }
}
$qb = new QueryBuilder();
$rows = $qb->query(array(
   field => ['id','name','cost'],
   table => 'products',
   where => 'id=' . $productId,
));

Now let us take a look at this example using named parameters. Hopefully you 
can see it is painfully verbose, definitely not DRY, and likely to result in 
typos or other refactoring errors: 
class QueryBuilder {

function query(string[] $fields, string $table, Join[] $joins, string[] $wheres 
):object[] {
      if ( ! $this->validate($fields, $table, $joins, $wheres ) ) {
         throw new Exception("Dude!");
      }
      $query = $this->build($fields, $table, $joins, $wheres);
      return $this->run($query);       
   }
   function validate(string[] $fields, string $table, Join[] $joins, string[] 
$wheres ) {
      ...
   }
   function build(string[] $fields, string $table, Join[] $joins, string[] 
$wheres ) {
      ...
   }
   ...
}

If we instead use object initializers, it becomes much readable and 
maintainable, even though the developer may have initially written the query() 
method by using named parameters:

And unlike using an array to contain all the values my IDE, PHP and other tools 
can validate whether or not I used the correct property names for Query when 
calling $qb->query():

class Query {
   public string[] $fields;
   public string $table; 
   public Join $join; 
   public string[] $where; 
}
class QueryBuilder {
   function query(Query $query):object[] {
      if ( ! $this->validate($query) ) {
         throw new Exception("Dude!");
      }
      $query = $this->build($query);
      return $this->run($query);
   }
   function validate(Query $query) {
      ...
   }
   function build(Query $query) {
      ...
   }
   ...
}
$qb = new QueryBuilder();
$rows = $qb->query(Query{
   fields => [ 'id', 'name', 'cost' ],
   table  => 'products',
   where  => 'id=' . $productId,
});

BTW, if I could have everything I want, I would really like following to work 
too:

$rows = $qb->query({
   fields => [ 'id', 'name', 'cost' ],
   table  => 'products',
   where  => 'id=' . $productId,
});

Note above I omitted Query{} and just used {} when calling $qb->query() because 
PHP should be able to see the signature for query() and pass the initialized 
values to instantiate a Query object instead of a stdClass instance.


-Mike

Reply via email to