Re: Pre-RFC Discussion: Support for String Literals as Object Properties and Named Parameters in PHP
| From: | Mike Schinkel | Date: | Wed, 04 Sep 2024 14:56:48 +0000 |
| Subject: | Re: Pre-RFC Discussion: Support for String Literals as Object Properties and Named Parameters in PHP | ||
| References: | 1 2 3 4 5 6 7 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-125414@lists.php.net to get a copy of this message | ||
> On Sep 2, 2024, at 11:10 PM, Hammed Ajao <hamiegold@gmail.com> wrote:
>
> That's one of the reasons, but arrays are not strict enough (you can add anything to any
> array, no distinction between a list and map), and you can't really specify the shape of an
> array.
What do you mean by "shape of an array" in this context?
I am trying to understand how an object differs in "shape" and I am coming up with
nothing.
> And/or do you want the compiler to validate when you use a name that has not been declared,
> i.e. when you type "Content_Type" or "Content Type" for <key> you want an
> error message?
>
> The cool thing is that the compiler already does this with named parameters or the splat
> operator applied to an array (https://3v4l.org/tholi#v8.3.11).
That is quite interesting, I did not know that was possible.
Unfortunately though, the validation is done at the point of call not at the point of array
instantiation which can result in spooky action at a distance if use in more than trivial ways:
https://3v4l.org/ZJXYX#v8.3.11
> Not at all, I just prefer custom and strict data structures with a clear intention.
+1
> I mean it is more work having to define a custom attribute and reflecting It's target each
> time you want literal string names. The compiler already does all I need, I just want to extend that
> support to literal strings.
>
> I'm proposing using a class every time you need a strict data structure with the keys
> known ahead of time, trust me i have no issue with more work when it's warranted, your version
> is just a lot of unnecessary code that doesn't actually achieve that.
Okay, I hear where you are coming from.
> I'm proposing two things:
> 1. Properties keys as literal strings:
> ```
> class A { public array "key"; }
> ```
> Since php also allows declaring properties in functions (promoted properties), we have to
> decide what to do with:
> ```
> class A { public function __construct(public array "key"); }
> ```
> Which means we would run into cases where it's valid to pass a literal string as a named
> parameter
> ```
> $a = new A("key": 1);
> ```
> I decided to add that to my proposal since there's no point restricting it to only
> constructors, which brings us to:
> 2. Literal strings as named parameters
> Since I decided to include functions, we now have to decide what to allow and what not, for
> example, can you now define params as strings too:
> ```
> function func(string "param", array "withDefault" = []) {}
> ```
> or is it only allowed in variadic functions and func_get_args() (so syntactic sugar for what we
> have now with func(...[]))
That is very helpful to understand why you came to what you are proposing.
For me though, extending it in that way would potentially infect other much more common use-cases so
I would prefer to have a constrained data type that allowed this rather than an open-ended one. And
one that did not perpetuate the requirement to use quotes for what are effectively identifiers that
do not follow the syntactical rules of identifiers.
> Say I have a response class:
> ```
> class Response {
> public function __construct(private MyHeaders $headers) {}
> }
> ```
> With MyHeaders being a class, nothing is stopping me from passing in:
> ```
> class MyCustomHeaders extend MyHeaders {
> public function __construct() {
> parent::__construct();
> //unset one of the keys or set more
> }
> }
> ```
> That isn't possible with the enum and I would have to store MyHeaders as an array in the
> Response class.
Fair point on the enum
OTOH as for storing as an array in your Response class, I don't see how that is huge issue. I
do see how the array does not have a reflectable identity, but stuff it in a single property
Response class and then together they do.
But I do get that you want to minimize that boilerplate.
> > MyHeaders::create("Content-Type": "text/html; charset=utf-8",
> > "X-Custom-Header": "value")
>
> But it's clear, each key does what it intends, no additional processing will be required
> to transform/validate the keys.
It is only "clear" if you can get past all the quotes obscuring what is a key and what it
a value.
When I first saw your proposal I did not notice the colons and thought you had written the
following. If you read back at my original reply you'll recognize that I misunderstood your
example:
MyHeaders::create("Content-Type", "text/html; charset=utf-8",
"X-Custom-Header", "value")
IOW, I find it too easy to misread and would not want to have to read and/or maintain code that did
not have a more easily visible distinction between what goes on the left of the colon and what goes
on the right, remembering that reading and maintenance comprise far more time in the life of working
code than the initial development.
> MyHeaders::create(Content-Type: "text/html; charset=utf-8", X-Custom-Header:
> "value")
>
> That works for me, but the literal has clearer intent and doesn't rely on transformation.
Clearer in your view, but not in mine.
Although I do concur that is requires a transformation I do not find that to be problematic as the
rules would need to be determinative and would be easier to recognize key vs. value. Especially
when reading code written by those darn developers who do not write each key:value pair on a new
line of their own. ;-)
It would be interesting to know who else on the list thinks it is clear enough with the quote vs.
those who would prefer a transform to avoid having to use quotes?
> Are you sure what you are proposing would _not_ end up having to use reflection under the hood
> and be equally less performant?
>
> Yep, it would be mostly handled in the parser/compiler, and internally, named params are
> already represented as a php array so we wouldn't even have to change much.
In your example which that I copied below for reference, how will
MyHeaders::create(...) and new self(...$headers) work without reflection?
class MyHeaders {
public function __construct(
public string "Content-Type" = "application/json",
public string "Cache-Control" = "no-cache, no-store, must-revalidate",
public string "Pragma" = "no-cache",
public string "Expires" = "0",
public string "X-Frame-Options" = "SAMEORIGIN",
public string "X-XSS-Protection" = "1; mode=block",
public string "X-Content-Type-Options" = "nosniff",
public string "Referrer-Policy" = "strict-origin-when-cross-origin",
public string "Access-Control-Allow-Origin" = "*",
public string "X-Custom-Header" = "value",
) {}
public static function create(string ...$headers): self {
return new self(...$headers); // Throws an error if an unknown named parameter is passed
}
public function dispatch(): void {
foreach ((array) $this as $name => $value) {
header("$name: $value");
}
}
}
$headers = new MyHeaders("Content-Type": "application/json",
"X-Custom-Header": "value");
// or
$headers = MyHeaders::create("Content-Type": "text/html; charset=utf-8",
"X-Custom-Header": "value");
$headers->dispatch();
> For example, what if PHP had two new datatypes DataSet and DataPair (or some other bikeshed
> names) that allowed for defining a set of name-valued pairs, that could be optionally be close-end
> or open-ended, that could be converted to arrays or stdClass objects, that could be validated by the
> compiler for close-ended sets, that could be used to specify named arguments for functions, that do
> not need to use quotes for names, and probably other very flexible aspects? Wouldn't that be
> better than shoehorning features onto existing classes to handle your use-case? (DataSet and
> DataPair could be classes under the hood, just like Enums are.)
>
> I would love more data types but that doesn't solve this issue.
I specifically crafted the idea of DataSet and DataPair to solve your
issues, at least as I understand them. Consider the following hypothetical example:
<?php
closed dataset MyHeaders {
public function __construct(
public datapair Content-Type = "application/json",
public datapair Cache-Control = "no-cache, no-store, must-revalidate",
public datapair Pragma = "no-cache",
public datapair int Expires = 0,
public datapair X-Frame-Options = "SAMEORIGIN",
public datapair X-XSS-Protection = "1; mode=block",
public datapair X-Content-Type-Options = "nosniff",
public datapair Referrer-Policy = "strict-origin-when-cross-origin",
public datapair Access-Control-Allow-Origin = "*",
public datapair X-Custom-Header = "value",
){}
public static function create(datapair ...$headers): self {
return new self(...$headers); // Throws an error if an unknown named parameter is passed
}
public function dispatch(): void {
foreach ((array) $this as $name => $value) {
header("$name: $value->current");
}
}
}
// Giving each header its own line so they are not so damn hard to read...
$headers = new MyHeaders(
Content-Type: "application/json",
X-Custom-Header: "value",
);
// or
$headers = MyHeaders::create(
Content-Type: "text/html; charset=utf-8",
X-Custom-Header: "value",
);
$headers->dispatch();
As I am proposing, the above defines a closed dataset meaning only the properties
declared are valid. If declared as open-ended, keys assigned to an instance would not need to be
validated in a constructor.
datapair properties and variables would be value objects with have a
->current property and an ->original property where the default
value would gets assigned to ->original and ->current, but
->current can be overwritten (or ->value and ->default if
default` could be used in this context.)
Anything declared to be a datapair type could be declared to have names that are not
valid identifiers and end with whitespace.
A declared datapair inner type would default to string but could be
declared otherwise if needed, see how I declared Expires as int. However,
it would be good if they could always just be string and never anything else.
By their nature, any dataset properties or variables would need to be accessed with braces, e.g.
$this->{Content-Type} and ${Content-Type} or if without quotes is not
possible in the parser then by $this->{'Content-Type'} and
${'Content-Type'}
Given the above — assuming it were accepted and added as a feature to PHP — how would this not
address your use-case(s)?
Note I have no idea if the above would even be considered, but I am proposing here to understand
what alternatives may be to address your desired use-case(s).
> Good call, I should probably separate both proposals.
+1
-Mike