Installing Larastan0:01
Now, if you're working in a Laravel project, I wouldn't recommend installing phpstan directly. As I said, Laravel has a lot of magic, and we benefit from that magic, but phpstan can't really understand the magic by itself, so we have to have something on top of phpstan that will help it understand how Laravel works. Thankfully, that already exists in a great package called Larastan, and you can install Larastan directly instead of worrying about installing phpstan first. So let's come down to the installation steps here. I'll jump into my terminal, and I'm going to composer require larastan, and you can see at the time of recording, it's greater than 2.0. Once that's installed, I'm going to create a phpstan.neon file at the root of my project, and let's open that file in our IDE so we can begin making the necessary changes.
Configuring phpstan.neon0:44
Once that's installed, I'm going to create a phpstan.neon file at the root of my project, and let's open that file in our IDE so we can begin making the necessary changes. So what goes inside our phpstan.neon configuration file? Well, the first thing we'll want to do is inform phpstan that we want to make use of Larastan, and we can use the includes node for this, which is an array of any extension files you want to make use of. In our case, we'll point it to vendor/Nuno Maduro/larastan.extension.neon. The next node we need is a parameters node, and this is where you configure the general usage of phpstan. For example, we can configure the paths that Larastan is going to search in when running static analysis. And for most applications, well, you only want to search inside your app directory, so the main directory that you use to write code in a Laravel application,
And for most applications, well, you only want to search inside your app directory, so the main directory that you use to write code in a Laravel application, that is where PHPStan is going to check for any type errors. The other important thing to configure is the level. So the cool thing about PHPStan is you can select the amount of static analysis that you would like to use. The levels range from one to nine. So one will check very basic things, mostly things that your IDE would pick up on anyway, whereas level nine is very strict checks, and you will be forced to write extra documentation code to help PHPStan understand how your application is structured, how your types are structured.
and you will be forced to write extra documentation code to help php Stan understand how your application is structured, how your types are structured. Now, for me and for this demonstration, I'm going to use level nine. If you're coming into an existing project where you want to add static analysis, you may find level nine to be just a little bit too much. So I would actually recommend level five for most projects, but if you want the most work but also the largest safety net, go ahead, try level nine, and see how your mileage varies. All right, we're going to come back to this file in later episodes, but why don't we go ahead and run vendor/bin/phpstan
Running Static Analysis2:44
All right, we're going to come back to this file in later episodes, but why don't we go ahead and run vendor/bin/phpstan in order to execute static analysis on the app directory of our application. Now, phpstan is performing a lot of work when you execute this command, so give it time to run. It may take a while, especially in larger applications, but once it has managed to execute once, it will actually cache a lot of the results, so subsequent runs should be a little bit faster. Once phpstan has analyzed your application,
Fixing Initial Type Errors3:10
so subsequent runs should be a little bit faster. Once phpStan has analyzed your application, it will likely come back with a bunch of errors that it has found that you can go ahead and fix. So let's take a look at the errors it's found in our HomeController here. The first one is fairly straightforward. It says that on line 10, there is a method called invoke, but we've not specified a return type. So one of the more basic things phpStan does is helps enforce using types wherever possible. Let's go into our HomeController, and sure enough, if I take a look at the signature for invoke,
Let's go into our HomeController, and sure enough, if I take a look at the signature for invoke, I don't have a return type specified. Now, in this case, I'm returning a view, so I can add view as the return type here. Illuminate\Contracts\View is probably the simplest, and then let's go ahead and rerun phpstan. When we do so, now we should have one less error to worry about. Now, if you look closely, you'll note that the errors for lines 19, 20, and 25 are the same. Cannot access property config on User or null.
Now, if you look closely, you'll note that the errors for lines 19, 20, and 25 are the same. Cannot access property config on User or null. We're going to come back to that in just a second. Let's focus on line 21 instead. So the first parameter that we've passed to the json_decode function should be a string, but technically, we could pass a string or null. And then the second error is that the second parameter we've passed to array_merge should be an array, but we've actually passed mixed. The first thing I like to do when fixing a phpStan issue is just work out why it's squawking in the first place.
The first thing I like to do when fixing a PHPStan issue is just work out why it's squawking in the first place. Why is it complaining? Well, of course, it said that the first parameter to json_decode should be a string, but we've passed string or null. Now, 99% of the time on the golden path, this will always be a string because we're getting the contents of a file. But the fact of the matter is Storage::get can return null if the file doesn't exist. So because of that, well, we need some check to make sure that we don't try to json_decode if there is no file called request_user_config,
So because of that, well, we need some check to make sure that we don't try to json_decode if there is no file called request_user_config, if the config that we've passed is incorrect. The second issue was with array_merge, and it said that the second parameter, which is this one here, should be an array, but it's mixed. And that's because json_decode doesn't have to return an array. json_decode could return null if the JSON is invalid. If I go to the browser, note that the page errors out. We have a runtime error here because it's saying that you can't array_merge on null.
Refactoring With Early Returns5:36
If I go to the browser, note that the page errors out. We have a runtime error here because it's saying that you can't array merge on null. And in the case of our config.json here, that's exactly what was taking place in the browser, right? That's exactly why the issue was being thrown in the first place. So thanks to phpStan, well, these issues have been highlighted before we ever shipped to production and saw this break for our users. Let's do a bit of refactoring to fix these two errors. The first thing I'm going to do is extract this here to its own closure. So let's say response equals,
The first thing I'm going to do is extract this here to its own closure. So let's say $response equals, and then I'll wrap this in a closure that is going to receive the $config and also the $name, and I'm going to slightly refactor this just to pass directly back the $name along with the $config. So now I could actually invert this, invert the if statement, and if the request $user does not have a $config, well then, of course, I'm able to say return $response, and I can pass it the $config, and I can also pass it the request $user $name like so.
and I can pass it the config, and I can also pass it the request user name like so. That allows me to remove this else statement we have here, and now our code is a little easier to read thanks to this early return that we've introduced. Now as we said, file content could well be null. So we want to make that check first. If not file content, return the response config request user name. Return early. There is no file content. The next thing we need to do is json_decode.
Return early. There is no file content. The next thing we need to do is json_decode. So I'm going to break out here, and I'm going to say $json equals json_decode($fileContent, true). We need to introduce another early return and say if not is_array($json, then, well, return early. Return response(config(request(userName))). And only, only if the $json is an array are we actually going to return our final response, which is obviously the updated config with the request(userName).
are we actually going to return our final response, which is obviously the updated config with the request user name. Obviously we've increased the complexity of our controller here by adding those extra early returns, but remember you'd have to do that anyway if you wanted to catch all the edge cases that your php code might throw. The only reason we even knew to add those early returns is because static analysis has reminded us that, well, our code could break under certain circumstances. Let's run php artisan again,
well, our code could break under certain circumstances. Let's run phpStan again, and we still have six errors, but note that all the errors have changed. They're now all basically focused on the fact that app\Models\User can also be null. Now, take your time with phpStan. Sometimes the error messages can be difficult to understand, to comprehend, but I promise if you stick with it, it will make sense after just a couple of days,
Handling Nullable Auth User8:22
but I promise if you stick with it, it will make sense after just a couple of days, and it will help you a lot in being able to debug issues that would otherwise come up in production. In the case of this error, well, we're making constant checks to requestUser, and the requestUser method can return null because you don't have to have an authenticated user. So what we should probably do here is extract the user to its own check. We can say $user equals requestUser,
So what we should probably do here is extract the User to its own check. We can say $user equals request()->user(), and then you can use a php function that you may not have used before in order to enforce the fact that the $user is an instance of the User class. So, again, you don't usually reach for assert in php code, but when it comes to phpStan, well, it will read that assertion as a promise, a guarantee that the $user variable is always an instance of our User model. Let's go ahead and refactor any instances of request()->user() to instead use this $user variable.
Let's go ahead and refactor any instances of request $user to instead use this $user variable. And once that's updated, well, let's go ahead and run phpStan again. Now you can see phpStan runs with no errors. In other words, the HomeController invoke method has no hidden side effects. There's no way that there is a type issue, a type error, something pops up during runtime that we've not already factored in inside our code here. Now, whilst this works, and we already see the benefit of phpStan in catching errors we'd otherwise have missed in our code base,
Now, whilst this works, and we already see the benefit of phpStan in catching errors we'd otherwise have missed in our code base, I'm definitely not a fan of lines 25 and 26 here. You see, we're using the auth middleware. We know for certain that the user will always be an instance of User. It will never be null. So this is redundant code only used for the sake of making phpStan happy. I like to try and minimize that where possible. The thing is, for something like this, it would make much more sense just to ignore those types of errors.
The thing is, for something like this, it would make much more sense just to ignore those types of errors, errors that check for User or null. And we can absolutely do that in phpStan from the phpStan.neon config file. So in the next episode, why don't we talk about how you can ignore errors in your application where it makes sense.
