Generics as Algebra0:00
Generics is such a scary and strange term. I think algebra is actually a nicer way to describe what generics is doing. You will provide a type, a type that doesn't actually exist, and when you run phpStan, well, it's going to switch that string, that type out with a real type that it can check against. Algebra, right? You replace numbers with letters, and it does the same thing. It accomplishes the same goal. Why don't we take a look at an example from Laravel itself of generics in phpStan, and then we'll build our own example in the application we've been using. For a moment, let's play around with collections so that we can see how generics are useful in this instance. I'm going to collect an array of, say, 1, 2, 3. Now, almost always, every item in a collection is the same type. In this case, they're all integers. Now, of course, when you collect something up, you likely are performing transformations on that collection. Maybe use the map method, for example. Now, map accepts a closure that receives the item, and then you return some other type.
Maybe use the map method, for example. Now, map accepts a closure that receives the item, and then you return some other type. Let's say, in this case, we return item times 2. So, this is going to return an integer. Now, what type is the item? Well, it's whatever type you've passed into the collection. So, in this case, it's going to be an integer. Now, php could care less what is passed into that closure. It doesn't know ahead of time what is passed into that closure. If we write object as the type int here, the only time we're going to see an error is when one is thrown at runtime,
It doesn't know ahead of time what is passed into that closure. If we write object as the type int here, the only time we're going to see an error is when one is thrown at runtime, inside the production environment, for example. This is where generics come in handy. Because if we can somehow tell phpStan that this type here is the same type that will be passed into the closure here, well, phpStan can make sure throughout our code base that that is actually the case, and we have not created some bug that we didn't know about. Well, it turns out you can tell phpStan all about that thanks to generic support. So, here's the Collection class in Laravel. And here we have this definition called template covariant T value.
Laravel Collection Templates2:15
So, here's the Collection class in Laravel. And here we have this definition called template covariant T value. Now, ignore the covariant for a moment. We're going to focus on template. Template is how you tell php that you want to define a new generic type. You can call the generic type anything you want. You can see the Laravel team have chosen T value here, but you could have called this T item. You could have called it T name, whatever you want. In this case, it's called T value, and that's how you'll reference it throughout the rest of the class. Now, where is T value actually used?
In this case, it's called TValue, and that's how you'll reference it throughout the rest of the class. Now, where is TValue actually used? Well, you can see when they define the underlying items that the collection wraps, they say array TKey TValue. You've seen this syntax before, just not with generics. We discussed it in the last episode. If I replace TKey with int and TValue with string, what are we saying? We're saying that this is an array of items where all of the keys are integers and all of the values are strings. The thing is, collections won't always be strings, and they won't always have keys which are integers, which is exactly why we need algebra, generics, replacements for the actual values.
The thing is, collections won't always be strings, and they won't always have keys which are integers, which is exactly why we need algebra, generics, replacements for the actual values that will be calculated when you're checking types, when you run phpStan. In this case, whatever type you pass to collect is the same type that will be passed here. This would be int and int, because obviously the keys are integers. It's a list, and the values are integers. Let's keep this in mind as we move down a little further. In fact, why don't we jump to the map method, since that's what we've been playing with. You'll see in the map documentation it defines another generic type called TmapValue. Let's just ignore that and take a look at the callable here.
Mapping Types with Generics3:55
You'll see in the map documentation it defines another generic type called TmapValue. Let's just ignore that and take a look at the callable here. We discussed shapes for closures and callables in the last episode, and it's saying that the first parameter that is passed is of type Tvalue. We've already discussed what is Tvalue. It's algebra. It's a generic for the initial type that was passed to the collection, an integer. This is just an int. You also know that when you call the map method in a collection, you can accept as a second parameter the key.
You also know that when you call the map method in a collection, you can accept as a second parameter the key. In this case, what would that be? It would be an integer, wouldn't it? int key. Again, that's what's happening here. Tkey is whatever the key type is when you first collect the array. This would be an integer. If you were to ungenerify it, if you were to take away the generics, this is the shape you'd be left with.
If you were to ungenerify it, if you were to take away the generics, this is the shape you'd be left with. Now, TmapValue is whatever you return from this callable, and that is why the template has been defined here rather than at the top of the class. So in our case, what are we returning? We're returning an integer. So phpStan, when you run it, is going to switch out TmapValue with int when it performs its checks in order to make sure that all of that is valid.
is going to switch out TmapValue with int when it performs its checks in order to make sure that all of that is valid. If we were to change this to string, and let's say we return the string val of item instead, well, now when you execute phpStan, it will switch TmapValue out with string. So now it expects a closure or callable, which receives an integer as the parameter. You can receive a second parameter, which is an integer, the key, and it's expected to return a string.
You can receive a second parameter, which is an integer, the key, and it's expected to return a string. If it doesn't, it's going to complain. Note that generics are also being used in the return statement here. So what is this saying? It's saying that the map method will return a new instance of the Collection class with the same key type as defined inside the initial collect. In this case, that's going to be an integer, right? Algebra, we replace the types with the actual values.
In this case, that's going to be an integer, right? Algebra, we replace the types with the actual values. And the value is going to be, well, whatever's defined here, whatever your return type is from the closure. In our case, the return type is a string, so phpStan at runtime will replace this with string in order to perform the checks. Does that give you an idea of why generics are important and how they work? Well, I hope so, but in order to try and cement this idea in your head,
Building a Generic Result6:28
and how they work? Well, I hope so, but in order to try and cement this idea in your head, why don't we write our own generic statements for the application that we were building in the last episode? Since the last episode, I've actually added a new action to our ProcessPodcast job called announceOnSocialMedia. You can see down here at the bottom, we do announceOnSocialMedia, we pass the path to the podcast, and then we want to say which social media platform we want to post to,
and then we want to say which social media platform we want to post to, and then finally we check that the status code was 200. If it wasn't, we're going to release for 60 seconds. Now, the general rule of thumb with all of these platforms is that when they return the result from post, they pass themselves into that result. And we've tried to enforce that a little by specifying that they have to return this contract type, but to be honest, that's not going to stop certain bugs.
by specifying that they have to return this contract type, but to be honest, that's not going to stop certain bugs from taking place in our code, because technically Twitter or X or something could return a new instance of Threads, and php would say, actually, that's completely valid. You're allowed to do that. Now, we know it shouldn't be doing that. It should actually return this. But yeah, it absolutely could return new Threads.
It should actually return this. But yeah, it absolutely could return new threads. php won't complain, and we'll see bugs in our code. Can we use generics to solve this problem? You bet we can. Let me show you. The first thing we'll need to do is make the Result class generic, and in order to do that, I can create a doc block at the top, and I'm going to use that template tag to define a new generic type for result.
and I'm going to use that template tag to define a new generic type for result. We'll call this tSocialMediaPlatform, and I want to limit it to only social media platforms, so I'll use the of keyword followed by SocialMediaPlatform. So this tells phpStan, look, this can be any type as long as it implements SocialMediaPlatform. If it doesn't, then it's invalid. The next thing we need to do
If it doesn't, then it's invalid. The next thing we need to do is tie this type to this property here. So I'll add a doc block above the constructor. I can get rid of the statusCode. That's not needed. And then rather than saying socialMediaPlatform, I'm going to say tSocialMediaPlatform. I'm going to use the generic type that I defined at the top of the class here. Okay, that is result handled.
that I defined at the top of the class here. Okay, that is result handled. This is now a generic class, and you can see that's the case by jumping into, say, SocialMediaPlatform, which returns result. phpStorm is showing a wiggly error underneath because there's a phpStan issue here, and sure enough, it says that the method post has a return type with a generic class, but it does not specify its types tSocialMediaPlatform.
has a return type with a generic class, but it does not specify its types tSocialMediaPlatform. So we need to update the doc blocks on this interface to tell it what the generic type is going to be when phpStan is executed. So I can get rid of this content from the doc block here. We then need to use the generic notation, which is less than, greater than. And, well, what is the generic type? It's whatever the current SocialMediaPlatform is.
And, well, what is the generic type? It's whatever the current social media platform is. So if we were using the Threads class, well, it's going to return a Threads result. That's the generic type. If we were using the Twitter or X or something class, it's going to return a result where the generic type is Twitter or X or something. Now, how do you define that in phpDocBlocks? We use the static keyword.
Fixing Covariance Errors9:46
Now, how do you define that in phpDocBlocks? We use the static keyword. We will return a result where the generic type is static. All right, now that we've added generics to these classes, let's run phpStan from the console and take a look at the errors returned. The two errors defined here are both inside the Threads and Twitter or X or something classes, and they're the exact same. Focus on this bottom part.
and they're the exact same. Focus on this bottom part. Template type tSocialMediaPlatform on class is not covariant. Now, I'm not going to go into why we need to worry about covariance in this lesson. This is an introduction to generics. But generally, if you see that error, you can actually go to the place where you define the generic, in this case, the result, and rather than saying template,
in this case, the result, and rather than saying template, you can say template covariant. Now, when we run vendor/bin/phpstan again, well, this is no longer going to fail, and we just have one error left. The error is on announceOnSocialMedia, so let's go ahead and open that class, announceOnSocialMedia, and sure enough, as you'd expect,
announceOnSocialMedia, and sure enough, as you'd expect, we have a squiggly line where the invoke method is. It's the same kind of message as before, right? Well, we've said that result is a generic class. That's what we return here. However, we haven't specified any of the generic types when we declare the invoke method. So let's add a doc block above the invoke method. We know that the path is going to be a non-empty string,
Method Templates and class-string11:07
So let's add a doc block above the invoke method. We know that the path is going to be a non-empty string, so we can actually go ahead and update that. The result is going to have to declare the generic type here. For now, let's just say SocialMediaPlatform, but I'm going to update that in just a second. The platform parameter is the fully qualified class name of the platform that we actually want to post to. So in the case of ProcessPodcast, we use Twitter or X or something,
So in the case of ProcessPodcast, we use Twitter or X or something, and you can see announceSocialMedia uses the app function to instantiate that platform, and then it calls the post method. Now, string is accurate, but there's actually a better way to declare this in phpStan, and that is class string. So anytime you're working with a fully qualified class name, you can use class string instead.
So anytime you're working with a fully qualified class name, you can use class string instead, which will help phpStan understand that this is not just any old string. It should be a correct, fully qualified class name. Now, class string is actually generic itself. You can say what type the class string is. So if we put SocialMediaPlatform in there, well, technically that's correct, right? It will be a class string of the type SocialMediaPlatform.
well, technically that's correct, right? It will be a class string of the type SocialMediaPlatform. To see this in action, let's go to ProcessPodcast, and instead of passing the fully qualified class name for Twitter or X or something, let's pass the fully qualified class name for Illuminate\Support\Collection, seeing as we've been discussing that in this lesson already. Now let's run vendor/bin/phpstan.
seeing as we've been discussing that in this lesson already. Now let's run vendor/bin/phpstan. Note that phpstan throws an error here because it expected a class string that extends or implements SocialMediaPlatform, but what we passed doesn't meet those requirements. So using class string can help you catch bugs like that. Let's return this to Twitter or X or something so that we can carry on with the example. Whilst this will work,
so that we can carry on with the example. Whilst this will work, it's not actually specific enough to allow phpStan to fully inspect our code. You see, these two things could be different instances. This could be a class string Twitter, and this could be a class string of Threads. In order to show you that that's the case, currently if I instantiate the Threads class, but inside ProcessPodcast,
currently if I instantiate the Threads class, but inside ProcessPodcast, obviously pass Twitter or X or something, and I run vendor/bin/phpstan, it isn't going to fail. It will be completely fine with the code we've written. We know that that's a bug, but yeah, phpstan has no clue. We can fix that by introducing yet another generic. So here at the top of the method,
We can fix that by introducing yet another generic. So here at the top of the method, I'm going to create a new template, and we'll call the template, I don't know, tSocialMediaPlatform, and again we can use the of keyword to narrow it down to only types that implement SocialMediaPlatform. Now, instead of using SocialMediaPlatform for the generics for class string and result,
Now, instead of using SocialMediaPlatform for the generics for class string and result, I'm going to use tSocialMediaPlatform. Now what is the difference? The difference is that now these have to be the exact same type for phpStan to pass. Let's go to the terminal and run vendor/bin/phpstan again. It throws an error. It has detected that, sure enough,
It throws an error. It has detected that, sure enough, in ProcessPodcast, you said you wanted an instance of Twitter or X or something, but we're actually returning an instance of Threads. It fails. It's not happy. It knows that something is wrong. Obviously, if we return this to the correct code and pass Platform instead, now the error disappears.
and pass Platform instead, now the error disappears, and phpstan is once again happy. So we can use generics to solve very complex edge cases like this one here. Let's now see if phpstan is able to catch the initial issue we highlighted. So the TwitterOrXOrSomething class is going to return a new result, but instead of passing itself in,
is going to return a new result, but instead of passing itself in, I'm going to return a new instance of Threads. php is fine with this. We know that this will cause a problem at runtime. Let's run phpStan, and hopefully, sure enough, it's found the issue thanks to generics. Post should return a result that has TwitterOrXOrSomething in it,
Post should return a result that has Twitter or X or something in it, but we actually return a result that has the Threads class instead. It can see there's a problem, and it tells us about it before we ship to production. Thanks to generics, we now know to return this to an instance of itself, to this. We have a correct implementation,
to an instance of itself, to this. We have a correct implementation, and VendorBin phpStan should pass with flying colors. So yeah, hands up, I get it. Generics are confusing. I'll be the first to admit that it took me ages to understand why I'd even want to use generics, never mind actually implementing them in php and phpStan.
never mind actually implementing them in php and phpStan. And I'm not expecting that by watching this episode, you're going to have a complete understanding of how generics work. It will take time and patience and practice before you're comfortable. But hopefully you've seen it's nothing more than algebra. You're simply removing an actual type, replacing it with a template type,
You're simply removing an actual type, replacing it with a template type, and allowing phpStan to do the reverse when you execute it to check for edge case bugs. And if you do put the time in to get comfortable with generics, I promise it will save your bacon many times over in the long run.
many times over in the long run.
