> On Mar 22, 2020, at 8:47 PM, Larry Garfield <la...@garfieldtech.com> wrote:
> https://hive.blog/php/@crell/improving-php-s-object-ergonomics

Hi Larry,

That is a really excellent writeup.  Thanks from me at least for taking the 
time to write it up in depth.

Looking at your conclusion, my gut feelings tell me your conclusion is about 
85% there, but with about 15% that gives me pause unless addressed, which I 
think is imminently possible. 

I'm going to do my best to explain what I am envisioning but realize I had not 
reserved time for this today so I am rushing to complete before my Monday 
starts in too few hours.  Given that I will almost certainly not be as clear in 
my thoughts had I had time to write and review it so please ask for 
clarification if something does not make sense. 

---

1. Your post mentions validation needed for value objects — a concern I share — 
but AFAICT your conclusion does not address the issue.

2. It also mentions overlapping concerns and references numerous RFCs, but 
there were two other relative works not mentioned. One[1] is an RFC and the 
other[2] a PR on Github. I think they both overlaps with this problem space, 
with the former addressing validation whereas the latter case potentially 
conflicts with constructor promotion.

3. And finally, some of the proposed concepts add syntax elements but do not 
make them first class language elements.

--- 

But before I cover those let me cover what I think you nailed to perfection:

1. The need resolve the verbosity of day-to-day programming with classes on PHP.
2. The distinction between Service and Value objects.
3. The value of immutability.
4. The downside of using arrays to pass properties (which is how many of us do 
it today.)
5. The benefits of contextual access
6. The limited nature of COPA/and write-once properties.
7. The elegance of property accessor syntax and how it can be applied to create 
readonly properties
8. The elegance of named parameters with the distinction between BC parameter 
passing and a newer JSON object-like syntax.
9. Rust-like cloning/Construct from syntax.
10. The problem of exposing parameter names as part of an external API for 
existing code
11. The problems associated with get and set properties actually running code 
when not expected by the user.

--- 

However, here is were I think your proposal still needs tightening up.

1. You don't really address the value object vs. service object distinction, 
except on the periphery.

2. You mention the concerns about exposing parameter names as part of the API 
but don't address those concerns directly.

3. You mention get/set properties surprisingly running code but do not address 
those concerns directly.

4. Your concept for a JSON object-like syntax for passing parameter feels 
incomplete to me. Unless I miss understand it is just a special syntax that 
only works in the context of passing arguments to a constructor, method or 
function, and not a first-class language element. If we were to go that route I 
think we would find it highly limiting.

5. In the section you say that "either of the following construction styles 
becomes possible" but you do not talk about the option of having one of more of 
the first parameters being positional and the rest being able to be passed by 
name, which I think would be an ideal use-case when you want to force certain 
parameters to always be passed but make the rest optional.

---

1. Let me now address the conflict with Nikita's "decorator" pattern support PR 
(which I and several others think would be better called delegation.) (As an 
aside, Go has such a feature and after having used it I find working with PHP's 
lack of delegation to be extremely painful.  Well at least we have Traits. You 
can see my comment on that PR and my proposal here[3] and [4].)

I think that delegation addresses the no-win scenario of and thus is an 
extremely important addition not to block:

- "Should I extend a class and have fragile base classes?" 
- "Should I use containment and then have a nightmare of boilerplate?", or
- "Should I use magic methods and loose performance and native ability use 
reflection and *_exists() functions?"

Unfortunately I think that Constructor Promotion assumes that all properties 
will be associated with the class at hand, and not with classes that are being 
delegated to. Yes we could assume they are joined together, and distributed to 
the various delegated instances, but I believe that could get very complicated 
very quickly.

2. Going further with constructor promotion, which you join with named 
parameters, how are the parameters represented inside the 
constructor/method/function?  As different variables methods just like current 
parameters?  Is there no concept of automatic aggregation of those parameters 
into some kind of structure that could then be passed down to other methods and 
functions?

3. And — I don't want to bike-shed — but I think the proposed constructor 
promotion syntax could result in potentially high levels of visual complexity 
that all must be syntactically correct across many different lines. That has 
the making of some very fragile code and code that is rather hard to follow, 
much like when developers write "metadata driven" code where they instantiate 
an array that contains many other arrays and the entire structure traverses 
several hundred lines of code or more.


----
One key language addition solves many of the above problems.

Imagine that we add a "structs" to PHP.  As I envision it:

A struct is a value object that has properties but either no methods or limited 
methods.

Structs don't need to be backward compatibility except not to conflict with 
existing syntax.

Structs are as simple as this (public is assumed for properties in my examples, 
unless otherwise specified):

struct Person {
   string $firstName
   string $lastName
}

Structs don't have constructors but do have an initializer syntax so Structs 
would be created like this:

$person = Person{firstName: 'Mike', lastName: 'Schinkel' }

This of a struct as a lightweight object with a few non-BC rules (like 
pass-by-value, more on that below). 

Struct properties could be accessed just like object properties, with the thin 
arrow ("->"):

echo $person->lastName;

Structs could potentially have get/set so as to allow read-only or write only 
properties:

struct Person {
   // This is read only
   private string $firstName {
        public get();
        private set();
   }
   // This is write only
   private string $lastName {
        private get();
        public set();
   }
}

Structs could possibly also support "Materialized Values" assuming those 
accessors were limited to only accessing other properties (but that might be 
really hard to implement):

struct Person {
   public string $firstName;
   public string $lastName;
   private string $fullName {
        public get(){
           return sprint('%s %s', $this->firstName, $this->lastName );
        }
   }
}

Structs could also be extended via classes.

class Developer extends Person {
    function work() {
         // writes code
    }
} 

Even better classes could delegate to Structs:

class Developer {
    use struct Person;
} 

And we could use aliases 

class Developer {
    use struct Person as person;
} 

And we could disambiguate

class DevelopmentManager {
    use class Manager;
    use class Developer {
       work as writeCode;
    }
} 

Further, we could support define named parameters by defining a Struct. Combine 
that with Construct-from syntax and it rid of the multi-line complexity and 
mess of the mess of Constructor Promotion:

namespace Structs;
struct Person {
   public string $firstName;
   public string $lastName;
}

namespace BizObjects;
class Person{
   use struct Structs\Person;
}

Better, we could allow sharing of names between a struct and a class (if in the 
same file) which could associate them:

struct Person {
   public string $firstName;
   public string $lastName;
}
class Person{
   use struct;
}

Which could be equivalent to:

class Person{
   use struct {
       public string $firstName;
       public string $lastName;
    }
}

Given the above the following function would only accept the object Person:

function renderPersonCard( Person $person ) {
    /// rendering...
}

This however could access the struct person:

function renderPersonCard( Person::struct $person ) {
    /// rendering...
}

With the above class/struct Person, you might create like this:

$person = new Person(Person{firstName: 'Mike', lastName: 'Schinkel' });

Or the shorthand:

$person = new Person({firstName: 'Mike', lastName: 'Schinkel' });

The above needs no explicit constructor, but what if we need to provide one?  
Note this example automatically initializes firstName and lastName:

class Person{
   private static $_people = array();
   use struct {
       public string $firstName;
       public string $lastName;
    }
    function __construct(Person::struct) {
      self::$_people[] = $this;
    }
}

Then there is the concern about parameters that are always required. They could 
be handled like this implicitly:

class Person{
   private static $_people = array();
   use struct {
       public int $personId;
       public string $firstName;
       public string $lastName;
    }
    function __construct(int $personId, Person::struct);
}

Or explicitly:

class Person{
   use struct {
       public int $personId;
       public string $firstName;
       public string $lastName;
    }
    function __construct(int $personId, Person::struct) {
      $this->personId = $personId;
    }
}

We could even move $personId out of the struct and into the class, if we wanted 
to:

class Person{
   public int $personId;
   use struct {
       public string $firstName;
       public string $lastName;
    }
    function __construct(int $personId, Person::struct) {
      $this->personId = $personId;
    }
}

But what about our development manager?  Note that a class would have an 
automatic Struct which would just be its properties:

class DevelopmentManager {
    use class Manager;
    use class Developer {
       work as writeCode;
    }
    function __construct(Manager::struct, Developer::struct) {
      self::$_people[] = $this;
    }
} 

The above would require instantiation like this:

$person = new DevelopmentManager(
        Manager{firstName: 'Mike', lastName: 'Schinkel' }, 
        Developer{ide:"PhpStorm", languages:["PHP","Go","SQL","etc.]}
);

Which could be shortened to:

$person = new DevelopmentManager(
        {firstName: 'Mike', lastName: 'Schinkel' },
        {ide:"PhpStorm", languages:["PHP","Go","SQL","etc.]}
);

Or with this construct:

class DevelopmentManager {
    use class Manager;
    use class Developer {
       work as writeCode;
    }
    function __construct(Manager::struct + Developer::struct) {
      self::$_people[] = $this;
    }
} 

Could be shortened further to:

$person = new DevelopmentManager({
        firstName: 'Mike', 
        lastName: 'Schinkel', 
        ide:"PhpStorm", 
        languages:["PHP","Go","SQL","etc."],
});

Structs could also support immutability by having them passed to methods and 
functions by value — like arrays — instead of by reference like objects.


In addition, if we embrace annotations we can address validation (note I'm 
using 'attribute(s)' keyword because <<>> created too much visual noise and 
annotation is longer IMO):

class Person{
   use struct {

       attributes NotEmpty, ProperCase
       public string $firstName;

       attributes NotEmpty, ProperCase
       public string $lastName;

       attribute ValidEmail
       public string $email;

       attributes CanBeEmpty, ValidPhone
       public string $phone;

    }
}

---
Notice how the inclusion if Structs solve so many of the edge cases of the 
original proposal, and provide significant new capabilities in a very elegant 
manner?  It addresses:

1. Value objects and possibly Materialized values
2. Need for backward compatibility with objects but not with structs
3. Unifying value objects with classes
4. By-value passing supports immutability
5. Delegation is not blocked
6. The "Named parameters" syntax is directly supported because it becomes the 
syntax to create struct instances.
7. It leverages Construct-from syntax
8. It reduces repeated references without forcing their declarations between 
contractor/method/function parentheses
9. Supports working with attributes/annotations better than Constructor 
promotion
10. And it is fully compatible with Get/Set property accessors.

So there you have it. I probably missed something and I probably was not clear 
enough in some area. Please do me the favor and if you have a question about 
how something will work first try to envision how it could work so you can 
suggest that when replying.

Looking forward to your thoughts. 

-Mike

P.S. Unfortunately with all the demand lately they seem to have run out of 
flame-retardant suits so I present this with no such protection.

---

[1] https://wiki.php.net/rfc/annotations_v2
[2] https://github.com/php/php-src/pull/5168
[3] https://github.com/php/php-src/pull/5168#issuecomment-586688715
[4] https://mikeschinkel.me/2020/adding-delegation-to-php/#summary

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to