> 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