-
Notifications
You must be signed in to change notification settings - Fork 4
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
PHPLIB-1250 Split encoders and fix psalm issues #46
Conversation
<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352"> | ||
<files psalm-version="5.20.0@3f284e96c9d9be6fe6b15c79416e1d1903dcfef4"> | ||
<file src="src/Builder/Encoder/AbstractExpressionEncoder.php"> | ||
<MixedAssignment> |
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.
A lot of MixedAssignment
calls are the result of incomplete type annotations throughout the generated files.
</MixedAssignment> | ||
</file> | ||
<file src="src/Builder/Encoder/FieldPathEncoder.php"> | ||
<NoInterfaceProperties> |
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.
Fixing this involves creating a new generator for field path classes so we can add a getName
interface methods. It irks me that public properties cannot be part of the interface contract, but here we are.
psalm-baseline.xml
Outdated
<code>$val</code> | ||
<code>$val</code> | ||
</MixedAssignment> | ||
<UndefinedConstant> |
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.
Similar to the interface property above, this undefined constant stems from us checking for an interface, then accessing a class constant.
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.
According to Object Interfaces: Constants, PHP 8.1 allows overriding constants. Is there any reason Psalm cannot support this?
Or does it stem from OperatorInterface not actually defining the constant, and it just being a convention in this library for implementing classes to do so? If so, perhaps we need to incorporate a new method for the encoding directive?
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.
Removed this entry and added a constant to OperatorInterface
.
</MixedAssignment> | ||
</file> | ||
<file src="src/Builder/Projection/ElemMatchOperator.php"> | ||
<MixedArgumentTypeCoercion> |
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.
MixedArgumentTypeCoercion
errors are due to the query expression not using the correct array element types but instead using the array
type. I tried to fix this, but the code generator doesn't accept a type like array<int|string>
. This will have to be looked at separately.
<PropertyTypeCoercion> | ||
<code>$expression</code> | ||
</PropertyTypeCoercion> | ||
<TooManyTemplateParams> |
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.
We use stdClass<foo|bar>
to indicate an object with an unknown number of properties of a specific type. However, stdClass
and object
cannot be templated this way, which is why we're seeing this error. The stdClass{foo: int}
syntax doesn't work for us here as we don't know the keys of the object, but only the expected value types. I've asked in the Symfony Slack to get more info about this and will update once I know more.
</TooManyTemplateParams> | ||
</file> | ||
<file src="src/Builder/Type/OutputWindow.php"> | ||
<DocblockTypeContradiction> |
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.
This and the RedundantConditionGivenDocblockType
checks are psalm telling us that the condition is impossible due to the docblock. However, since we're being extra cautious here and any user not using psalm is able to send any types they want, I think it's fair to keep this around unless we want to accept potential fatal errors when users abuse this.
return $value instanceof Pipeline; | ||
} | ||
|
||
public function encode(mixed $value): stdClass|array|string |
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.
This returns always a list.
public function encode(mixed $value): stdClass|array|string | |
public function encode(mixed $value): array |
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 this case, would list
be preferable?
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 changed the return type to array
, and the resulting type to list<array|stdClass|string>
as list
itself wasn't sufficient. Since we don't have BuilderEncoder::encode
typed any more strictly, this is the best we can do.
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.
Correction: it's list<mixed>
, as encodeIfSupported
is used throughout the encoders, which results in an effective return type of mixed
whenever we defer to the BuilderEncoder
class.
|
||
public function encode(mixed $value): stdClass|array|string | ||
{ | ||
if (! $this->canEncode($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.
We need a method encodeIfSupported
to avoid repeating this 3 lines in all encoders.
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 encodeIfSupported()
already provided via the EncodeIfSupported trait above? The trait implementation utilizes the canEncode()
and encode()
methods.
I understand that doesn't address the repetition of checking canEncode()
in each encode()
method, though.
src/Builder/BuilderEncoder.php
Outdated
$encoder = $this->getEncoderFor($value); | ||
|
||
return $result; | ||
return $encoder !== null && $encoder->canEncode($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.
There is syntax for that:
return (bool) $this->getEncoderFor($value)?->canEncode($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.
Updated, along with a later check.
$result->{$key} = $this->encodeIfSupported($value); | ||
} | ||
if (! $encoder || ! $encoder->canEncode($value)) { | ||
throw UnsupportedValueException::invalidEncodableValue($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.
The $encoder->canEncode($value)
part is already checked inside $encoder->encode($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.
Yes, however there is no guarantee that somebody will never call encode
with an invalid value, as the contract does not require calling canEncode
for a value beforehand.
We could solve this with the introduction of a null
type for the $value
argument in the Encoder::encode
definition, but that would require us to always accept null
values in any encoder, which might still necessitate calling canEncode
. void
as PHP's true bottom type is unfortunately not available as an argument type, so there's no proper typing way around this.
return $value instanceof Pipeline; | ||
} | ||
|
||
public function encode(mixed $value): stdClass|array|string |
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 this case, would list
be preferable?
|
||
public function encode(mixed $value): stdClass|array|string | ||
{ | ||
if (! $this->canEncode($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.
Isn't encodeIfSupported()
already provided via the EncodeIfSupported trait above? The trait implementation utilizes the canEncode()
and encode()
methods.
I understand that doesn't address the repetition of checking canEncode()
in each encode()
method, though.
psalm-baseline.xml
Outdated
<code>$val</code> | ||
<code>$val</code> | ||
</MixedAssignment> | ||
<UndefinedConstant> |
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.
According to Object Interfaces: Constants, PHP 8.1 allows overriding constants. Is there any reason Psalm cannot support this?
Or does it stem from OperatorInterface not actually defining the constant, and it just being a convention in this library for implementing classes to do so? If so, perhaps we need to incorporate a new method for the encoding directive?
$object = new stdClass(); | ||
$object->{$value->getOperator()} = $result; | ||
|
||
return $object; |
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.
Do we want a one-liner for this?
$object = new stdClass(); | |
$object->{$value->getOperator()} = $result; | |
return $object; | |
return (object) [$value->getOperator() => $result]; |
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'd like to microbench both implementations.
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.
Deferring to @GromNaN for 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.
Since I was already microbenching array_key_exists
, I also checked this. There's no significant performance difference (30 ms when testing 1M object creations), so there's no performance benefit in using either.
throw UnsupportedValueException::invalidEncodableValue($value); | ||
} | ||
|
||
return '$$' . $value->name; |
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 FieldPathEncoder you have the following:
// TODO: needs method because of interface
Is this also affected? If so, can we cover both in a single JIRA ticket?
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.
@alcaeus: ☝️
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.
Tracked in PHPLIB-1379.
b5dba67
to
f65ba10
Compare
f65ba10
to
75da7ee
Compare
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.
LGTM, but not my outstanding question about TODO comments.
@@ -9,5 +9,8 @@ | |||
*/ | |||
interface OperatorInterface | |||
{ | |||
/** To be overridden by implementing classes */ | |||
public const ENCODE = Encode::Single; |
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.
Noted that you're defaulting to Single
here instead of introducing an invalid case. My thinking was the invalid case would make it easier to catch mistakes when failing to override this, since Single
would just happen to work (at least for encoding) provided the class only had a single 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.
You can add a Undefined
case to the enum. We could also use null
, but the const may be typed one day.
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'm personally a fan of Undefined
but won't push for it.
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.
Introduced Undefined
value to the enum. Agree with not abusing null
for this to keep the constant strictly typed.
Co-authored-by: Jeremy Mikola <jmikola@gmail.com>
6dfae19
to
c49a256
Compare
PHPLIB-1250
This PR splits the main
BuilderEncoder
into multiple files. This allows for more isolation of separate code (which I'm sure can be improved further), but also allows for easy introduction of custom codecs (currently not tested yet).In addition, this PR cleans up a number of psalm issues and adds other issues to the baseline. I'll comment on those as necessary.