Re: Improving PHP's Object Egonomics: A broad analysis
| From: | Mike Schinkel | Date: | Mon, 23 Mar 2020 06:07:25 +0000 |
| Subject: | Re: Improving PHP's Object Egonomics: A broad analysis | ||
| References: | 1 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-109223@lists.php.net to get a copy of this message | ||
> On Mar 22, 2020, at 8:47 PM, Larry Garfield <larry@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