PHPStan finds missing types0:00
Look, we've been using PHPStan as though it's a family sedan. In reality, it's a powerful sports car. Let's take it for a real spin. I have this background task called ProcessPodcast in my application, and it is in charge of taking these three actions, join clips, tune audio, upload to podcast platforms, and passing in the correct parameters to prepare a podcast that we've recorded for the mainstream. If I run vendor/bin/phpstan from the console again, you'll see I actually already have errors just from writing the code that I have shown you so far. And that's because, certainly in the higher rule levels of PHPStan, it expects you to provide additional information for certain parameter types. Let's take a look at join clips, for example. It says that the invoke has a parameter called
Typing arrays with docblocks0:47
certain parameter types. Let's take a look at join clips, for example. It says that the invoke has a parameter called clips, but no value type has been specified in the iterable type array. What is that talking about? Well, here is the clips parameter, and yes, it is an array, but it's an array of what? That is what PHPStan is asking. What is this an array of? It has no clue what's inside the array. Now, we've inferred that it is going to be an array of strings. So, how do we tell PHPStan about that? Well, we do it using good old PHP doc blocks. So, above the method, I've created the corresponding doc blocks, and here you can see, yeah, we have the definition for array clips. Now, currently, this provides no additional information to what we already have on line 18, but we can use the less than, greater than symbols
have the definition for array clips. Now, currently, this provides no additional information to what we already have on line 18, but we can use the less than, greater than symbols to provide additional context to PHPStan. Now, in this case, we want to say that the key type, so the type of each key in the array, is integer, because this is going to be a list, and the value is going to be a string. So, let's just go over this syntax again. This is an array where all the keys are integers, and all the values are strings. Thinking about it further, all of the values have to be non-empty strings. They cannot contain an empty string. It cannot just be quote marks. So, in PHPStan, we can say non-empty string, and that will ensure it will check that every string has an actual value included. Very useful little helper. The other cool shortcut is if you know the
non-empty string, and that will ensure it will check that every string has an actual value included. Very useful little helper. The other cool shortcut is if you know the array is a list, that is to say, a sequential keyed index starting from zero, you can use list instead of array, and you can get rid of the key type here. So, the clips array is actually a list of non-empty strings. Pretty cool. Running vendor/bin/phpstan again shows us that, yeah, the error has disappeared. We have fixed the type check for that parameter. Of course, the other parameters are still broken, so options has no value type specified. Now, this has to be slightly different to our list non-empty string, because not all of the values are going to be the same type. They're not all going to be strings. You might have an intval. You might have a boolean for an option. You might have
Defining array shapes3:06
because not all of the values are going to be the same type. They're not all going to be strings. You might have an integer. You might have a boolean for an option. You might have an object for an option. However, we likely know the array shape ahead of time. To support array shapes, you can use the curly brace syntax, and then I'm going to provide a list of keys and the types of values. So, what options should we have here? Well, perhaps we need to be able to state whether the clips should be joined in a sequential fashion, so one after the other, or whether they should be placed one on top of the other for two sides of a conversation, for example. So, let's create an option called type, and maybe type is a string. So, you can say sequential or parallel. If there are only a certain set of options and you're not using an enum, you want to use a string, you can actually tell
type is a string. So, you can say sequential or parallel. If there are only a certain set of options and you're not using an enum, you want to use a string, you can actually tell PHPStan exactly which strings are supported. So, here I could say you can make it sequential or you can make it parallel. And if I pass any string that isn't sequential or parallel, well, PHPStan is going to fail. Now, of course, our options should be optional. We don't have to pass a type because it will have a default. If that's the case, then you can follow the key with a question mark, and PHPStan will now view this as an optional parameter to make use of. Perhaps another option we support is whether to crossfade the clips, and that would be a boolean, right? So, we could say crossfade. Again, we probably want to make this optional, so I'll add the question mark, and the type is a boolean. So, we separate
would be a boolean, right? So, we could say crossfade. Again, we probably want to make this optional, so I'll add the question mark, and the type is a boolean. So, we separate parameters with a comma, just as we do when we're actually defining parameters inside a method signature. With our options defined, we should be able to run vendor/bin/phpstan again, and yeah, you can see we now only have a single error for joinClips. What is the error? Well, it should return a string, but the return statement is missing. In other words, we're not returning anything in here. Let's return an empty string for now. Of course, we don't really want to return an empty string. We should enforce returning a non-empty string because you have to return a valid path to the podcast. So, why don't we update the return doc block here to say, well, it returns a non-empty string, and for now, I'll place
because you have to return a valid path to the podcast. So, why don't we update the return doc block here to say, well, it returns a non-empty string, and for now, I'll place foo in there so that phpStan doesn't complain when we run it. By the way, obviously you can run phpStan from the console, but if you're using PHPStorm, go into the settings, go to PHP Quality Tools, and there's actually phpStan support built right in. So, I'm going to turn this on, click Apply, and click OK, and now, wherever there's an issue with phpStan, it'll show up with a little squiggly line. For example, if I return an empty string here, it'll show up with a little squiggly line telling me that I have an issue with phpStan. So, that's so much more useful than having to keep jumping into the terminal to see what's wrong, and if you hover over the squiggle, you can see underneath exactly what the error is. Super useful if you're
more useful than having to keep jumping into the terminal to see what's wrong, and if you hover over the squiggle, you can see underneath exactly what the error is. Super useful if you're using PHPStorm. Let's return this to foo, and let's go into our next action and fix that. Now, if I hover over this squiggly line here, you'll see that the issue is the same as before. You should return a string, but the return statement is missing. So, again, let's return an empty string for TuneAudio, and now PHPStan has stopped complaining. All of this is valid. However, we can go further and make this more clear. We can make our intent more clear to PHPStan, and that will help prevent errors that might otherwise pop up unnecessarily. I'm going to add the doc block to the top of the invoke method once more, and first of all, let's return a non-empty string again because it has to be a valid path, and once more,
Constrained integer ranges6:43
I'm going to add the doc block to the top of the invoke method once more, and first of all, let's return a non-empty string again because it has to be a valid path, and once more, I'll return foo to get rid of the error for now. Of course, the path that is passed to invoke here also should be non-empty, so I'll pass a non-empty string there. Next, let's give consideration to this integer, which represents the volume adjustment we want to make. Given that the default is 100, we can assume that 100 is the current volume level. So, if we set this to 0, the audio would be silent. If we set it to 200, it would be twice as loud. Now, it's actually possible to enforce constraints on an integer in PHPStan. So, we don't want to allow negative integers, for example. If you just want to block negative integers, you can use the positive int doc block, and now anything in the minus range will be blocked by PHPStan when you.
negative integers, for example. If you just want to block negative integers, you can use the positive int doc block, and now anything in the minus range will be blocked by PHPStan when you run it. Of course, you can also use negative int if you want the opposite to be true. However, if you want to be more specific, you can just pass int, and you can use the less than or greater than symbols again to pass a min and a max. So, let's say we want to avoid being able to be silent. The lowest adjustment you can make is 1, and the highest adjustment is 200. So, now we've set to PHPStan, look, when I pass an integer to invoke, if it's less than 1, throw an error. That's not correct. If it's greater than 200, throw an error, but anything in between those numbers or those numbers themselves is absolutely fine. Allow that. Of course, for speed adjustments, we probably want to be a little more fluid. Of course, we don't want negative integers because
numbers or those numbers themselves is absolutely fine. Allow that. Of course, for speed adjustments, we probably want to be a little more fluid. Of course, we don't want negative integers because zero would be no speed at all, but we don't want to set an upper limit. There are two ways to do this. Obviously, you can use positive int, as we already mentioned, but you can also say int, and let's say 1 is the minimum speed, and max. So, max is a special constant used by PHPStan that basically sets it to infinity, any number greater than 0. So, as you can see, PHPStan isn't just about fixing the errors that PHPStan throws at you. It's about giving it the extra information needed for it to be able to understand how your application works, and that is how it protects you down the line. How much information you add is completely up to you, but the more information you add, well, the more PHPStan will be able to help you out and ensure that everything
protects you down the line. How much information you add is completely up to you, but the more information you add, well, the more php VendorBin PHPStan will be able to help you out and ensure that everything works as you'd expect. Running VendorBin PHPStan once more shows that we have just one error left inside our UploadToPodcastPlatforms action, so let's go and sort that one now. Hovering over our squiggly line again tells us that, yeah, we've not said what actually goes in the podcastPlatforms array. So, let's add another doc block here. First of all, we could say that the string path is a non-empty string. Once again, it has to be a valid path. It doesn't return anything, but that adds no extra information to the type int we have here, so we'll just remove that for now. The array is actually a list, as we said before, of podcastPlatform, and podcastPlatform is an enum I have that just allows me to specify which platform I want to upload to. Now, once again,
Typing closure signatures9:50
The array is actually a list, as we said before, of podcast platform, and podcast platform is an enum I have that just allows me to specify which platform I want to upload to. Now, once again, all the errors have gone here, but I think this closure could be improved significantly. You see, at the moment, we don't really know what the closure accepts. We don't know what the closure should return. If we look at the information up here, it says that this closure will be called throughout the upload process and will be passed the current progress percentage. Let's assume that's an integer. Returning false from the callback will cancel the upload, so we can return a boolean here, and that will actually affect this action. It would be good to have phpStan understand how this closure is put together, so that if we use the closure incorrectly, well, we're told about that. Now, in the same way that you can create array shapes, as we did earlier.
understand how this closure is put together, so that if we use the closure incorrectly, well, we're told about that. Now, in the same way that you can create array shapes, as we did earlier with our options, you can also create closure shapes. You can define what a closure should look like. So, after declaring the closure type here, I'm going to add brackets, and I'm going to pass in the type of the first parameter, in this case, an integer. Now, we can pass as many parameters as make sense, so the second parameter might be a string, the third parameter might be a boolean, but in this case, the callback receives a single parameter, which is the current percentage of the upload. In order to specify the return type, well, after the closing bracket, I'm going to add a colon, followed by a space, followed by the type that I need to return, in this case, a boolean. You can see when I do that in phpStorm, at least, well, it highlights it correctly, because this is
colon, followed by a space, followed by the type that I need to return, in this case, a boolean. You can see when I do that in phpStorm, at least, well, it highlights it correctly, because this is now a valid closure syntax. Now, it might be that I want to allow returning null, which would just be the same as returning true from this closure. In order to do that, I need to wrap the return type in brackets to group it together, and then I can use the or symbol and pass null as a second parameter type. So, we now know, and more importantly, phpStorm now knows that any callback given here has to accept an integer and has to return a boolean or null. If we run vendor/bin/phpStorm once more, you can see that everything passes, which means that we've declared the types on all of these objects correctly, and we're ready to start implementing them inside our Qt job. So, we'll begin by implementing our joinClips action, and again, let's check what it accepts,
Testing types in IDE12:17
on all of these objects correctly, and we're ready to start implementing them inside our Qt job. So, we'll begin by implementing our joinClips action, and again, let's check what it accepts, an array of clips, which have to be non-empty strings, and it has to be a list type, and then an optional array of options where we can pass the type and crossfade. So, let's play around with this for a moment and see what happens when I pass an array with an empty string in it. Well, you'll see I have a squiggly line pop up, and when I hover over it, it's going to say, parameter one of clips expects an array that is a non-empty string, but we have passed an array that is an empty string. If I was to fill this string out, let's say clip1.mp3 and clip2.mp3, well, yeah, now the error disappears because we're satisfying phpStance requirements. Let's now play with the options that we can pass to joinClips. I'm going to pass in an array,
well, yeah, now the error disappears because we're satisfying phpStan requirements. Let's now play with the options that we can pass to joinClips. I'm going to pass in an array, and I'll say type, but I'm not going to pass parallel or sequential. I'm just going to pass in the string foo. Now, it's a string, but sure enough, phpStan is going to complain, and it's going to say, look, I expected parallel or sequential, but you passed foo in, and I can't do anything with that. So, let's update this to be sequential, and then we'll move on to crossfade. By the way, check out what phpStorm is able to do. Because we have defined an array shape, it can suggest the keys that we might want to use. So, not only does phpStan help improve your type checking, but it also improves your IDE auto-completion, which is a great added bonus. Let's select crossfade, and let's set it to something like
help improve your type checking, but it also improves your IDE auto-completion, which is a great added bonus. Let's select crossfade, and let's set it to something like 1 instead of a boolean. If we tab away, well, again, phpStan is going to complain and say, it should be a bool. I'll set it to false, and the phpStan error should disappear. Now, of course, we know that JoinClips returns to us a string, which is the path of the saved joined clip. So, let's say path and set that to a variable, and then we can move on to TuneAudio. Let's invoke the TuneAudio action, and I have to pass in that path. Again, the TuneAudio path is a non-empty string, but because the return type of JoinClips is non-empty, it isn't going to complain. If we were to just pass a string in like so, obviously, it would start to squawk because you have given a non-empty string. So, again, if you're working
it isn't going to complain. If we were to just pass a string in like so, obviously, it would start to squawk because you have given a non-empty string. So, again, if you're working with non-empty strings, make sure that your functions and methods that you create correctly return a non-empty string type so that phpStan doesn't complain unnecessarily. Now, remember, for TuneAudio, we set limits to the volume you're allowed to pass and the speed that you're allowed to pass. So, let's see what happens when we play around with these values. For volume, let's set it to 0, a volume of 0, and yes, phpStan has complained. You can see that it tells us it has to be an integer between 1 and 200. If I set this to 201, we'll get very much the same error because, once again, 201 is above the max of 200. If I set this to 200, well, now it's not going to complain anymore. That is a valid value. If I set
we'll get very much the same error because, once again, 201 is above the max of 200. If I set this to 200, well, now it's not going to complain anymore. That is a valid value. If I set this to 1, it isn't going to complain anymore. That, too, is a valid value. For speed, well, again, if I set it to 0, it will complain, but I could set any maximum I wanted, say 1000, and you'll see it doesn't complain here because there is no maximum for this. I can set it to whatever makes sense for my application. Once again, we return a non-empty string in this action, which is the new path of the saved audio file. So, I'll update the path variable to be the return type of TuneAudio. Finally, let's move on to Upload to Podcast Platforms. I'll call the action. I can pass in the non-empty string as the path. I then have to pass an array of podcast platforms I want to upload to. Take a look again at what we created here. It's a list of podcast platforms.
I can pass in the non-empty string as the path. I then have to pass an array of podcast platforms I want to upload to. Take a look again at what we created here. It's a list of podcast platforms. So, it cannot have string keys. It cannot have indexes that aren't sequential and don't start at 0. Otherwise, phpStan will complain. So, if I tried to say, for example, foo equals podcast platform Apple, well, phpStan is going to moan at that because it has to be a type of list. You can see that in the second error that appears just here. If I remove the string key, well, I'll still get a complaint because we haven't passed the third parameter, but it's no longer complaining about this podcast platform that we've passed here. And of course, I can go ahead and select another podcast platform if I want to upload to multiple places. Let's separate these on their own lines. The third parameter I have to pass is that closure.
And of course, I can go ahead and select another podcast platform if I want to upload to multiple places. Let's separate these on their own lines. The third parameter I have to pass is that closure that I defined. Now, let's go ahead and incorrectly declare the closure. So, I'll accept a path, which is a string. That's obviously not the case. And I'll return foo. And let's see what phpStan says. I get the squiggly line, as I'd expect. If I hover over, it's going to say that it expects a closure, which receives an integer and returns either a Boolean or null, but we passed a closure that was given a string and returns foo. That won't do. And unless you fix it, phpStan will fail. So, let's update this to be an integer that accepts progress. And we need to return, let's say true in this instance. We just want it to run and continue uploading to the various platforms. Well, the IDE isn't complaining anymore, but just for full measure, let's go ahead and run.
let's say true in this instance. We just want it to run and continue uploading to the various platforms. Well, the IDE isn't complaining anymore, but just for full measure, let's go ahead and run phpstan once more. And sure enough, there are no errors with our code. But at any point, because we've told phpstan all this extra information about our actions, if we do something that isn't supported, perhaps if we misspell sequential, well, phpstan will start to complain. You'll see the errors in CI, and you won't be able to ship to production. That is unless you ignore CI, which is down to you as a developer, rather than down to phpstan. So, isn't that cool that you can give phpstan this information, that you can add that extra information about your integers, about your booleans, about closures, about array shapes, and phpstan is able to take all that in, scan your code, check for usages, and then tell you if you've misused the code that
integers, about your Booleans, about closures, about array shapes, and phpStan is able to take all that in, scan your code, check for usages, and then tell you if you've misused the code that you've written. It stops you making mistakes, and also it adds to and improves your documentation, which is a huge win for developer experience. Now, there's one more advanced topic that I want to touch on when it comes to phpStan, and that is generics. It can be a scary topic, but I'll try and break it down as best as I can in the next episode. Let's take a look.
