Skip to content

PHPLIB-955: Require PHP 8.1 #1374

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

Merged
merged 12 commits into from
Sep 9, 2024
Merged

Conversation

alcaeus
Copy link
Member

@alcaeus alcaeus commented Sep 4, 2024

PHPLIB-955

The first commit updates the requirement in composer.json and updates CI configs. In the second commit, I applied all automated fixes from phpcbf. This mainly affects the use of union types as well as the mixed type.

A couple of notes on phpcs rules:

  • The "DisallowedShortNullable" rule was disabled, as it would change all ?Type types to Type|null. I prefer the short syntax over the consistency with other union types (e.g. Type|Other|null)
  • The rule requiring constructor property promotion was disabled as it caused some invalid syntax, for example by adding a default value to a constructor parameter when other parameters after it don't have a default value. I'll investigate and create a bug ticket for this.

@alcaeus alcaeus requested a review from GromNaN September 5, 2024 07:53
@alcaeus alcaeus marked this pull request as ready for review September 5, 2024 07:53
@alcaeus alcaeus requested a review from a team as a code owner September 5, 2024 07:53
@alcaeus
Copy link
Member Author

alcaeus commented Sep 5, 2024

Marking this ready for review. I'll revisit the failing tests shortly, as I suspect those will be fixed by #1373

@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This directive is necessary to disable type coercion in testConstructorOutArgumentTypeCheck; all of the invalid values provided there can be coerced to strings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that the validation is weaker than before, since the values were not cast. Do we consider that it's the user's responsibility to send the right type and use strict mode if they wish to avoid this kind of error? In any case, it won't work with an int casted to string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change, users could pass an int which we'd have to guard against. With the string type enforced by PHP, there are two scenarios: if the calling file has strict_types disabled, an int would be coerced to string; with strict_types enabled a TypeError is thrown. The end result is the same: the method will not be called with a non-string parameter.

* @psalm-param BSONType $value
* @return mixed
* @psalm-return NativeType
* @throws UnsupportedValueException if the decoder does not support the value
*/
abstract public function decode($value);
abstract public function decode(mixed $value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed this is not a breaking change for classes that override this abstract method. https://3v4l.org/donQG

I don't get why Symfony BC promise prohibit adding typehint to arguments in classes. https://symfony.com/doc/current/contributing/code/bc.html#changing-classes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Without a type specified for an argument, people can't add their own, narrower types. When we add a type, people are still allowed to widen types or omit the type entirely, which is why we can add parameter types in classes and interfaces in minor versions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/me looks over at the Covariance and Contravariance poster hanging on my wall.

@@ -54,8 +54,7 @@ class StreamWrapper
/** @var resource|null Stream context (set by PHP) */
public $context;

/** @var ReadableStream|WritableStream|null */
private $stream;
private ReadableStream|WritableStream|null $stream = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the type on properties produces an error in case of access to uninitialized property. That's why you added the default null value here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct 👍

Copy link
Member

@jmikola jmikola Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does PHP raise an error for the uninitialized property? I'm curious if the error is only triggered by explicitly assigning null in the property declaration. If so, would you rather do without that and omit |null from the type?

See: https://3v4l.org/RHWMH#v8.3.11

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the type was only ReadableStream|WritableStream without a default value, accessing $this->stream would cause a fatal error. I believe we could theoretically use that instead of assert in a number of places, as even with disabled assertion handling any access to the stream would result in an object access on null, which again yields a fatal error.

I'll note that isset does not trigger the uninitialised property error, so we would still be able to check if a stream was initialised. I can change this if y'all prefer to leave null out of the type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add on to this, I'll note that psalm does not like properties without a default value that aren't initialised in the constructor:

ERROR: MissingConstructor - src/GridFS/StreamWrapper.php:56:43 - MongoDB\GridFS\StreamWrapper has an uninitialized property MongoDB\GridFS\StreamWrapper::$stream, but no constructor (see https://psalm.dev/073)
    private ReadableStream|WritableStream $stream;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to leave as-is. Seems like the path of least resistance.

@@ -250,7 +243,8 @@ protected function getInvalidBooleanValues(bool $includeNull = false): array
*/
protected function getInvalidDocumentValues(bool $includeNull = false): array
{
return array_merge([123, 3.14, 'foo', true, PackedArray::fromPHP([])], $includeNull ? [null] : []);
return array_merge([123, 3.14, 'foo', true], $includeNull ? [null] : []);
// return array_merge([123, 3.14, 'foo', true, PackedArray::fromPHP([])], $includeNull ? [null] : []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You left this commented.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, removed.

For context, with the object|array type now enforced for documents, PackedArray instances no longer cause an error. I could bring back the PackedArray check if we want to be more specific about what objects are allowed, but I defer to @jmikola on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the PackedArray check still in place? I saw you removed various conditionals when adding array|object type hints, but is_document() still rejects PackedArray instances. I would expect that that InvalidArgumentException::expectedDocumentType() still gets thrown for PackedArray instances.

I assume changing InvalidArgumentException expectations to TypeError is what conflicts here. I don't feel strongly about this, but I'd argue that parameter and option type checks shouldn't necessarily be expected to raise TypeError. There are still various other contexts where we raise InvalidArgumentException for validation errors (e.g. option value is out of range, or a string value doesn't satisfy our homemade enum).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed. I've updated the type assertions based on the value given to expect a TypeError or InvalidArgumentException and restored the PackedArray instance as an invalid document, as it adds confidence that we have the right checks in place.

With TypeError being a thing, I wonder if we should consider throwing TypeError throwables instead of InvalidArgumentException on type mismatches.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With TypeError being a thing, I wonder if we should consider throwing TypeError throwables instead of InvalidArgumentException on type mismatches.

That sounds like a great idea, now that we can reliably expect TypeErrors from PHP itself. Might be something to consider for both PHPC and PHPLIB, and then we can also remove the vapid documentation for throwing InvalidArgumentException from most things.

@alcaeus alcaeus requested review from jmikola, GromNaN and a team September 5, 2024 07:55
Copy link
Member

@GromNaN GromNaN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

</MoreSpecificReturnType>
</file>
<file src="src/Model/BSONIterator.php">
<MixedArgument>
<code><![CDATA[$this->options['typeMap']]]></code>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we define array shape for the options?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had that. However, in the param declaration, it would have to be array{typeMap?: array, ...}, whereas for the property it's array{typeMap: array, ...}. With promoted properties, the same type is used for the parameter and property, leading to this error. Instead of adding custom logic to work around this, I decided to add the error to the baseline.

@@ -397,7 +397,7 @@ private static function getVersion(): string
if (self::$version === null) {
try {
self::$version = InstalledVersions::getPrettyVersion('mongodb/mongodb') ?? 'unknown';
} catch (Throwable $t) {
} catch (Throwable) {
Copy link
Member

@jmikola jmikola Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted that the variable is no longer required as of PHP 8.0 (see: Exceptions).

@@ -250,7 +243,8 @@ protected function getInvalidBooleanValues(bool $includeNull = false): array
*/
protected function getInvalidDocumentValues(bool $includeNull = false): array
{
return array_merge([123, 3.14, 'foo', true, PackedArray::fromPHP([])], $includeNull ? [null] : []);
return array_merge([123, 3.14, 'foo', true], $includeNull ? [null] : []);
// return array_merge([123, 3.14, 'foo', true, PackedArray::fromPHP([])], $includeNull ? [null] : []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the PackedArray check still in place? I saw you removed various conditionals when adding array|object type hints, but is_document() still rejects PackedArray instances. I would expect that that InvalidArgumentException::expectedDocumentType() still gets thrown for PackedArray instances.

I assume changing InvalidArgumentException expectations to TypeError is what conflicts here. I don't feel strongly about this, but I'd argue that parameter and option type checks shouldn't necessarily be expected to raise TypeError. There are still various other contexts where we raise InvalidArgumentException for validation errors (e.g. option value is out of range, or a string value doesn't satisfy our homemade enum).

@alcaeus alcaeus force-pushed the phplib-955-require-php-8.1 branch from bf2e611 to 48c14de Compare September 6, 2024 09:32
@alcaeus alcaeus requested a review from jmikola September 6, 2024 11:54
@alcaeus alcaeus force-pushed the phplib-955-require-php-8.1 branch from 48c14de to a44c71f Compare September 9, 2024 06:15
@alcaeus alcaeus enabled auto-merge (squash) September 9, 2024 06:15
@alcaeus alcaeus merged commit 24214b6 into mongodb:master Sep 9, 2024
30 checks passed
@alcaeus alcaeus deleted the phplib-955-require-php-8.1 branch September 9, 2024 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants