-
Notifications
You must be signed in to change notification settings - Fork 235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Default property with @NamedArgumentConstructor #402
Conversation
docs/en/custom.rst
Outdated
@@ -100,7 +100,8 @@ you can simplify this to: | |||
} | |||
|
|||
Alternatively, you can annotate your annotation class with | |||
``@NamedArgumentConstructor`` in case you cannot use the marker interface. | |||
``@NamedArgumentConstructor`` in case you cannot use the marker interface. | |||
In this case, the first argument of the constructor will be considered as the default property. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this is written makes me think that this works only when using the @NamedArgumentConstructor
annotation but not when using the marker interface, as you added that in the paragraph about the alternative. Is it actually true ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is true. It is the intended behaviour as we didn't want to introduce breaking change with current implementation of the interface.
See #396
I see @derrabus mentioned that we should deprecate the interface, and I quite agree, I'd rather not have 2 ways of doing the same thing, with differences that are hard to grasp (the only comment on this PR so far is about exactly that BTW). Did you overlook that or is it planned for later? |
@greg0ire Ok, the interface has been deprecated and the doc updated. |
Please kindly squash your commits together. Maybe not all, but some definitely needs to be squashed. If you don't, we'll try to remember to do it for you but it's best if you save us this trouble. How to do that?
|
Use the first constructor argument as default property when @NamedArgumentConstructor is used
@greg0ire Done! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An idea to improve the patch a little to be less invasive.
@@ -859,7 +863,17 @@ private function Annotation() | |||
); | |||
} | |||
|
|||
$values = $this->MethodCall(); | |||
$defaultProperty = 'value'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this code block and passing down $defaultProperty
could you instead handle this logic in the original line 944 and following?
foreach ($values as $property => $value) {
if ($property === 'value') {
$property = self::$annotationMetadata[$name]['default_property'];
}
That would be less invasive and keep the logic at fewer places. I am not 100% sure it could work this way though, just a rough idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @beberlei! Thx for the review. I didn't want to pass the defaultProperty
down. The problem is, without it, we don't know with the return of values()
, if value
as been set because it's the default property... or because we have an argument named
value
. So it would not work if for example the constructor looks like this:
public function __construct(string $name, int $value){}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But isnt that already a problem? Or just for the new named constructors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave a bunch of examples here : #396.
The fact that we can't know if value
has been set because it's the default property or because we have an argument with the same name, is, indeed an "old problem".
But we didn't want to change the current behaviour to avoid BC breaks, so we decide to handle it properly only with the new @NamedArgumentsConstructor
annotation as it is not published yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Problem is all methods with large letter starting are named after the rules in the grammer. None of them accept arguments, because they already operate on state of the class instead. Passing arguments now will create a huge inconsistency in the API.
What you could try instead is change the return value of Values
to be an associative array with: ['named_arguments' => [], 'positional_arguments' => []]
and then modify Annotation
to act accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@beberlei Here is my first attempt trying what you suggested.
I was also wondering why this is allowed: @Name(@Name, @Name)
, but this is not @Name("1", "2")
. The second case is not allowed because of this line:
throw $this->syntaxError('Value', $token); |
In our case, it means that we can only use one positional argument. It would make sense for us to allow multiple positional arguments as it's also how attributes work.
#[Name("1", "2")]
would work for example as long as the related attribute constructor accept at least two arguments.Removing the above line don't break the tests and allow multiple positional arguments.
What do you think about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In our case, it means that we can only use one positional argument. It would make sense for us to allow multiple positional arguments as it's also how attributes work.
Very well, but Doctrine Annotations only supports one positional argument: the default property. I would consider it out of scope of this PR to introduce the ability to have multiple ones.
Besides, a developer using an annotation does not know whether it has been implemented with a named constructor or not (and they should not need to know, imho). It would be kind of odd if multiple positional arguments only worked in the named argument constructor case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, I'm a bit late to the party and I see that the discussion has already moved on. I'm fine with allowing multiple positional arguments. Thank you for working on this feature. 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@derrabus : Actually, Annotations already support multiple positional arguments. It's just that without @NamedArgumentConstructor
all positional values will be populated as an array into value
. This @Name(@Name, @Name)
already work. So in a developer point of view, it doesn't really change anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better! Only some small things left from my pov, then good to merge
} | ||
} else { | ||
$values = $namedArguments; | ||
if (! empty($positionalArguments) && ! isset($values['value'])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use count instead, empty does not communicate intent well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't count() > 0
much less effective than !empty()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but the output of this code is usually cached and even if not, this is not a tight loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to toy with phpbench so I build a little benchmark from this raw benchmark:
<?php
class ArrayCheckerBench
{
public function provideArrays(): \Generator
{
yield 'empty array' => ['array' => []];
yield 'huge array' => ['array' => range(0, 100000)];
}
public function checkArrayWithEmpty(array $x): bool
{
return empty($x);
}
public function checkArrayWithCount(array $x): bool
{
return count($x) === 0;
}
/**
* @Revs(1000000)
* @Iterations(5)
* @ParamProviders({"provideArrays"})
*/
public function benchCheckIfArrayIsEmptyWithEmpty(array $parameters): void
{
$this->checkArrayWithEmpty($parameters['array']);
}
/**
* @Revs(1000000)
* @Iterations(5)
* @ParamProviders({"provideArrays"})
*/
public function benchCheckIfArrayIsEmptyWithCount(array $parameters): void
{
$this->checkArrayWithCount($parameters['array']);
}
}
Here are the results:
vendor/bin/phpbench run ArrayCheckerBench.php --report=aggregate --retry-threshold=0.5
PhpBench @git_tag@. Running benchmarks.
\ArrayCheckerBench
benchCheckIfArrayIsEmptyWithEmpty # emp.I4 [μ Mo]/r: 0.285 0.285 (μs) [μSD μRSD]/r: 0.000μs 0.06%
benchCheckIfArrayIsEmptyWithEmpty # hug.R1 I1 [μ Mo]/r: 0.287 0.287 (μs) [μSD μRSD]/r: 0.000μs 0.12%
benchCheckIfArrayIsEmptyWithCount # emp.R1 I1 [μ Mo]/r: 0.309 0.309 (μs) [μSD μRSD]/r: 0.001μs 0.28%
benchCheckIfArrayIsEmptyWithCount # hug.R3 I3 [μ Mo]/r: 0.311 0.311 (μs) [μSD μRSD]/r: 0.001μs 0.34%
2 subjects, 20 iterations, 4,000,000 revs, 0 rejects, 0 failures, 0 warnings
(best [mean mode] worst) = 0.285 [0.298 0.298] 0.285 (μs)
⅀T: 5.958μs μSD/r 0.001μs μRSD/r: 0.201%
suite: 134621f1b80d1bb3f7ed3eaf6bc32c7bb639d7d1, date: 2021-02-07, stime: 15:08:37
+-------------------+-----------------------------------+-------------+---------+-----+-------------+---------+---------+---------+---------+---------+--------+-------+
| benchmark | subject | set | revs | its | mem_peak | best | mean | mode | worst | stdev | rstdev | diff |
+-------------------+-----------------------------------+-------------+---------+-----+-------------+---------+---------+---------+---------+---------+--------+-------+
| ArrayCheckerBench | benchCheckIfArrayIsEmptyWithEmpty | empty array | 1000000 | 5 | 526,184b | 0.285μs | 0.285μs | 0.285μs | 0.285μs | 0.000μs | 0.06% | 1.00x |
| ArrayCheckerBench | benchCheckIfArrayIsEmptyWithEmpty | huge array | 1000000 | 5 | 64,194,552b | 0.287μs | 0.287μs | 0.287μs | 0.288μs | 0.000μs | 0.12% | 1.01x |
| ArrayCheckerBench | benchCheckIfArrayIsEmptyWithCount | empty array | 1000000 | 5 | 526,184b | 0.308μs | 0.309μs | 0.309μs | 0.310μs | 0.001μs | 0.28% | 1.08x |
| ArrayCheckerBench | benchCheckIfArrayIsEmptyWithCount | huge array | 1000000 | 5 | 64,194,552b | 0.310μs | 0.311μs | 0.311μs | 0.313μs | 0.001μs | 0.34% | 1.09x |
+-------------------+-----------------------------------+-------------+---------+-----+-------------+---------+---------+---------+---------+---------+--------+-------+
Looks like count takes almost 10% more time, so in relative terms, it's far better indeed. In absolute terms though, it seems to be 30 ns slower… I don't think we should worry about such small differences.
$positionalArguments = $arguments['positional_arguments'] ?? []; | ||
$namedArguments = $arguments['named_arguments'] ?? []; | ||
|
||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you extract this block into a private helper please?
$values = $this->assembleValues($arguments, $name);
Method name could be better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
I was also wondering why this is allowed:
In our case, it means that we can only use one positional argument. It would make sense for us to allow multiple positional arguments as it's also how attributes work. #[Name("1", "2")] would work for example as long as the related attribute constructor accept at least two arguments.Removing the above line don't break the tests suite and allow multiple positional arguments. What do you guys think about this? |
@Vincz this only works for the new named argument ctor attributes though or? Because otherwise multiple values are handled as an array of |
I think it would allow |
Ok |
Ok, I'll add a bunch of tests then and make sure it doesn't break anything. |
So, it is now possible to use With the |
($lastPosition === null && $position !== 0 ) || | ||
($lastPosition !== null && $position !== $lastPosition + 1) | ||
) { | ||
throw $this->syntaxError('Positional arguments after named arguments is not allowed'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw $this->syntaxError('Positional arguments after named arguments is not allowed'); | |
throw $this->syntaxError('Positional arguments are not allowed after named arguments'); |
} | ||
|
||
return $values; | ||
return ['named_arguments' => $namedArguments, 'positional_arguments' => $positionalArguments]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In resolvePositionalValues
, you validate $positionalArguments
. Can't we do this here already? I don't like that we pass around potentially invalid metadata.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Values()
method is part of the parsing process. If we move the validation inside, it will have to know what kind of annotation we are talking about. The validation is different is you use @NamedArgumentConstructor
or not.
This is valid without @Name("1", foo="bar", "2")
but should not be valid with @NamedArgumentConstructor
as it's supposed to mimic a regular constructor call with named arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, thank you for clarifying. 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good! I think we got a good state now that is ready to merge and then ship
} else { | ||
if (count($positionalArguments) > 0 && ! isset($values['value'])) { | ||
if (count($positionalArguments) === 1) { | ||
$value = $positionalArguments[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #405
Use the first constructor argument as default property when @NamedArgumentConstructor is used.