-
Notifications
You must be signed in to change notification settings - Fork 821
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
Introduce declare(strict_types) to force strict typing on builtin method calls #9101
Conversation
The unit tests need We have a problem. The framework contains many type inconsistencies. diff --git a/src/Control/HTTPRequest.php b/src/Control/HTTPRequest.php
index 77446332b..0bd7b299b 100644
--- a/src/Control/HTTPRequest.php
+++ b/src/Control/HTTPRequest.php
@@ -877,9 +877,8 @@ class HTTPRequest implements ArrayAccess
user_error('HTTPRequest::detect_method(): Invalid "_method" parameter', E_USER_ERROR);
}
return strtoupper($postVars['_method']);
- } else {
- return $origMethod;
}
+ return $origMethod;
}
/**
diff --git a/src/Control/NullHTTPRequest.php b/src/Control/NullHTTPRequest.php
index 5d9e98667..4b0030501 100644
--- a/src/Control/NullHTTPRequest.php
+++ b/src/Control/NullHTTPRequest.php
@@ -12,6 +12,6 @@ class NullHTTPRequest extends HTTPRequest
public function __construct()
{
- parent::__construct(null, null);
+ parent::__construct('', '');
}
}
diff --git a/src/Core/ClassInfo.php b/src/Core/ClassInfo.php
index 586db7aa6..76d552513 100644
--- a/src/Core/ClassInfo.php
+++ b/src/Core/ClassInfo.php
@@ -356,7 +356,7 @@ class ClassInfo
}
if (!array_key_exists($lMethod, self::$_cache_methods[$lClass])) {
- self::$_cache_methods[$lClass][$lMethod] = false;
+ self::$_cache_methods[$lClass][$lMethod] = '';
$classRef = new ReflectionClass($class);
diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php
index d1a4e1bbd..85432b279 100644
--- a/src/Core/Injector/Injector.php
+++ b/src/Core/Injector/Injector.php
@@ -419,7 +419,7 @@ class Injector implements ContainerInterface
}
// okay, actually include it now we know we're going to use it
- if (file_exists($file)) {
+ if ($file !== null && file_exists($file)) {
require_once $file;
}
diff --git a/src/Core/Manifest/ManifestFileFinder.php b/src/Core/Manifest/ManifestFileFinder.php
index b8bd6604e..8d5600b45 100644
--- a/src/Core/Manifest/ManifestFileFinder.php
+++ b/src/Core/Manifest/ManifestFileFinder.php
@@ -197,7 +197,7 @@ class ManifestFileFinder extends FileFinder
protected function upLevels($pathname, $depth)
{
if ($depth < 0) {
- return null;
+ return '';
}
while ($depth--) {
$pathname = dirname($pathname); |
Yeah I think there's going to need to be some effort involved here to adjust the methods to have consistent return types, while adding upgrade rules to the upgrader and changelogs to note that these are changing in 5.x |
You have the advantage of knowing every bit of the framework. |
I wish I did 😆 @silverstripe/core-team how would you like to handle the transition to strict typing? This PR goes a long way towards it already. There are cases we'll need to change, and in those can we take the happy path return type and use that? E.g. |
return+else may never execute :) |
One (hard) way is to run unit tests and get one error. PHPStan (on a certain higher level) may give you a nice list of all errors. |
Can we apply a bit of caution and fully understand the effects of this before merging? This is effectively going to break every module and project built with SilverStripe. Since the API wasn't built with this in mind, my worry is that it affects us in much bigger ways than we're expecting here. ie. we've allowed concrete classes to be replaced by injector without any type checking in the past. This probably requires genuine refactoring rather just running an upgrader tool. For reference, there is some push back from implementing strict typing throughout Laravel: https://twitter.com/taylorotwell/status/1139501976180285442 |
It is known that there are two opposing groups of developers. One group builds its robust code on strict typing, the other group wishes to be done asap. |
In general, stricter usage of types would be a good thing. To your point about class overrides, those should either be subclasses of the class be overridden, or the service definition should be an interface, not a class. However, in SS4 I don't think we want to change the return types of our public API, even if doing so would make the return types more consistent. We could consider this for SS5, although we'd only want to do so if the upgrade impact on developers would be minimal. In the case of the SS_BASE_URL example, a change like this would be fine for SS4, rather than making Enviornment::getEnv() consistently return a string. Frankly, regardless of return type, this should probably be the code - passing an empty value to parse_url to trigger a no-op isn't great.
Rolling out declare(strict_types) seems like a good idea, but it will require a fair bit of work. As @szepeviktor hints at - this is a codebase going back to 2005 and at that time PHP wasn't really a language that worried about type hygiene (and it's lead dev was a lot less experienced :p). In general we should look to introduce type hygiene but not at the cost of massive upgrade pain. |
Per @robbieaverill 's comment on the original issue: Yes please, into the master branch :-) Can we retarget this? |
Perhaps let's resolve the discussion of what we plan to do here before going through the rigamole of re-targeting.
|
This is how to replicate this PR composer require slevomat/coding-standard
vendor/bin/phpcbf --standard=vendor/slevomat/coding-standard/SlevomatCodingStandard --sniffs=SlevomatCodingStandard.TypeHints.DeclareStrictTypes src/ tests/ then revert |
So one issue with introducing this is that without a lot of guards and type coercion, badly-typed inputs from 3rd party code will then throw issues. We have a couple of choices:
The most common "badly typed input" appears to be that a null/empty value is passed when a string is expected. So a lot of it comes down to adding Here's a sample the changes needed - these are the minimum changes needed to get DatabaseTest to pass. https://github.com/sminnee/silverstripe-framework/commit/1c82eb89d58b3ebcb7eaa67b163c853bddc9e6f5 |
Developing a widely used framework may be very-very hard. I think most of the users do not think about variable types. PHPStan will complain 1000× This is already a string, why checking? |
...broadening this to get the whole of ORM tests passing wasn't too bad. |
We generally don't run tests with unexpected input types which are currently coerced by PHP though, so I'm not sure how much that's proving.
Going through the entire framework or supported modules API surface (both public and protected, due to subclassing/DI) seems like a giant amount of work? We can't really use a fuzzer with varying inputs either, because it would have no idea on how to correctly instanciate classes. We could infer from PHPDoc
How is this discussion related to return type declarations (PHP 7.1 and newer) and argument type declarations (PHP 7.0 and newer). Since we require PHP 7.1 as of SS 4.5, we can now introduce those. It looks like we can retroactively add those type declarations without breaking subclasses or existing function invocations (unless they're using the wrong type already of course). This code doesn't throw on PHP 7.3: <?php
class C {}
class A {
function foo(int $int, C $c) {
// ...
}
}
class B extends A {
function foo($int, $c) {
// ...
}
}
$c = new C();
$a = new A();
$a->foo(1, $c);
$b = new B();
$b->foo(1, $c); Note that <?php
declare(strict_types=1);
function foo($int) {
var_dump($int * 2); // outputs 2, only throws with an explicit type declaration on the argument
}
foo(true); |
Since we supported PHP 5.x until now in SS 4.x, the only valid type declarations in our code can be Class/Interface, So the question is: How much breakage do we expect from usage of this subset of existing type declarations?
So in conclusion, I think we might be able to do set Some more test cases below (executed on PHP 7.3) <?php
// class type checks - throws
class B {}
class C {}
function foo(B $b) {
}
$c = new C();
foo($c);
// callable type checks - throws
function foo(callable $fn) {
$fn();
}
$fn = function() {
echo 'foo';
};
foo(null);
// array type checks - throws
class MyIterator implements IteratorAggregate
{
private $items = [1,2,3];
public function getIterator() {
return new ArrayIterator($this->items);
}
}
function foo(array $arr) {
foreach($arr as $item) {
echo $item;
}
}
foo(new MyIterator());
// array string type checks - throws
function foo(array $arr) {
foreach($arr as $item) {
echo $item;
}
}
foo('123'); |
It is very easy to show what strict types hold for us: execute these 2 commands #9101 (comment) on the framework and try running the unit tests. Every breakage will show what type difference strict types prevents. my first one was ini_set() 9d8f6e1 |
Yeah, I agree with this. Notably it throws E_STRICT errors for people overriding such methods in a subclass. We could add it for new APIs, and I think we could get away with adding it in SS5, especially if we could automatically rewrite any method override definitions. Adding declare(strict_types) is a bit different, in that it effectively puts type-hints on builtin methods for calls to those methods. Naïvely it seems like it's not going to break APIs, because it only affects calls made from the file in which the declaration is set. The problem comes with cases such as this: Imagine this is our legacy code: <?php
function fiveLengths($input) {
return strlen($input) * 5;
}
fiveLengths("hello"); // 25
fiveLengths(234); // 15 Adding declare_strict(types) is tantamount to putting a type hint on fiveLengths: <?php
declare(strict_types = 1);
function fiveLengths($input) {
return strlen($input) * 5;
}
fiveLengths("hello"); // 25
fiveLengths(234); // error Which you can avoid with appropriate guards: <?php
declare(strict_types = 1);
function fiveLengths($input) {
return strlen((string)$input) * 5;
}
fiveLengths("hello"); // 25
fiveLengths(234); // error This, however, reduces the benefit, although we could still be fussy about types in internal code. But the full benefit really comes if you tidy up your type hinting as a major change: <?php
declare(strict_types = 1);
function fiveLengths(string $input): int {
return strlen($input) * 5;
}
fiveLengths("hello"); // 25
fiveLengths(234); // error |
I've changed the title of this as I believe the previous title was confusing people :p |
I wonder if it's better to complete #9103 before picking this up? It'll be easier to dive into strict type compliance with phpstan there to support us... |
Closing this PR. |
Fixes #9040
@robbieaverill You could add phpcs sniff in the issue to sustain strict typing.