A balanced approach to static analysis in Laravel apps
Welcome to No Compromises. A peek into the mind of two old web devs who have seen some things. This is Joel.
And this is Aaron.
I have grown to appreciate having stronger, stricter types in PHP. And I will admit that while I see the benefit of it, it can occasionally feel a little tedious. Especially when you're working in a framework like Laravel that has some really nice convenience things that maybe don't play nicely with static analysis. So, I thought today we could kind of talk through a couple of specific examples, get a little nerdy. Where either we worked around some typing limitations in Laravel to make the experience better or other places where we're like, "You know what? I don't care that... I'm not going to worry about that type checking." And kind of the balance we strike in those two approaches.
Yeah, I think that that's a good topic. One of the issues I run into is, with PHP we know it can be loose typed. So, how much typing do we do? If the typing isn't working, should I just remove all that? And which sets of equals should I use? And is it coming from the browser or an API? There's so many different things there. I mean, this could be a giant topic. But what I think you're kind of talking about is either static analysis or convenience helpers in our code that make it just a little bit easier for us to do our work.
Right.
I think I can give an example. One of the things that we kind of talked about before is there's PHPDoc that you can put in line with your functionality. I think you've mentioned this in your tips podcast or newsletter and stuff. But you can basically put a particular @var sign defining variable into something else. And that won't affect the programmatic part of it, but it will affect IDE or any sort of intelligence or anything like that, that you have analyzing your code. Whether it's PHPStan or your IDE and basically say that this variable is now of this other type.
Right, we use those. You might look at it and you might say, "Ah. What? You're just adding this stuff into your code just to make the tool happy?" or whatever. But that's a direction that we've found, yes, it adds some things to the code that are just to make a tool happy, but it's not too invasive and it's not changing how the code runs and we're okay with that. We don't go nuts. You're not going to have like 30 `@var` docblocks for every variable ideally, most of them to actually be statically analyzed correctly on their own but in some cases, we'll throw one in there. That's a compromise we've lived with. I want to bring up another example if this is okay. And it was one we were just looking at together, which is where Laravel does provide a type but it's not quite the type we want. So, to get specific. If you're in a controller and you have the request and you say, `$request->user` you get a user, but it's not a `User`. You want to talk through that example?
Yeah. When you do that, you're going to get basically an interface. They've type hinted that with a return with an interface. So the interface or a contract, if you're going to be abstract about it. Well, this is a horrible word to use I'm talking about. But a contract in programming parlance is returned so that identifies according to the framework. You basically call a framework method and the framework is saying, "Well, of the things that I know about a user, I only know these public things that we've already defined that you must have because of the contract. So I'll just return an instance of that." Whereas they can't know specifically in your application all the different things you've done on the user or different methods you added, properties, all that kind of stuff. And they shouldn't know that that's where the difference is. So, when you call $request->user, you get what looks like an instance of that contract, but it's missing all of the additional functionality. And let's be clear, you can still use the functionality on it.
Sure, it works. Yeah, tests will pass.
It won't auto-complete, your static analysis won't work. So that's that problem.
Yeah. So kind of leading to the previous thing we were talking about, one solution would be anytime you do $request->user, you sign it to a variable or you put a var docblock and you say, "Nope, this is an \App\Models\User, not Illuminate\Foundation\Authenticatable," or whatever it has normally. But I like the solution you came up with. And I'm going to share it because I'm going to tell everybody it was my idea.
Okay.
No. But it was just a very simple trait. It was a trait, right? Called ProvidesAuthenticatedUser. I probably should have pulled up the code. But anyways, this simple trait, I think just had one method on it, which was... Was it getUser or was it just user or it was?
I think it's getAuthenticatedUser.
Okay. But it was one method and all it return was Auth::user. Because we don't know what context you're going to be calling this in. But the only benefit is that it had a return type of \App\Model\User. So now we can, by bringing this trait in at a high level, we're not bringing this trait into hundreds of places. You could use that method and get the user type and not have to have a var docblock. Did I explain it correctly?
Yeah. And since it's a trait, we can use it in the controller if we're going to work with something there. You can use it in request class directly, all that different stuff. It's kind of one of the solutions. Now, again, all these things are specific to your use case and you don't want to make a bunch of getter functions just to change the return types either. Since it's a trade, you can import it into the controller or into the request classes, anything like that, so you can use that wherever you need.
I like it. I'm going to throw out one more example and this is a place where maybe I pushed the boundary and you're like-
Oh. Actually, can I interrupt you? I got one quick aside.
Okay.
You know it's been kind of on my mind there. There's something you said earlier that has been hanging in my mind. You said something about making sure that the tool is... You're not just making changes to make the tool happy. And if you know me, one of the questions I always ask is, why? Like, "Why is that? Why X, Y, Z?" So you're right sometimes we'd make these changes to make the tool happy. But I remember when we were working together too, you would make a change or something and you'd be like, "Hey, I did this because now PHPStan is passing." And I didn't like the code change so I'd say, "But why?" And if your answer was, "It would make the code standards." I'm not okay with that, that's not cool.
Like, if you didn't have a deeper understanding as to why the types didn't line up or what the tradeoff was?
Right. So, I just wanted to make sure we covered that too. Is like some of those things we got to make sure we understand the why behind them.
No, I think that's actually... And before you saying that, it didn't quite crystallize for me that way. But it's not just whether we're making a tool happy or not, but it's like, are you just doing something to make an error go away and you don't really know what's going on? That's where we feel uncomfortable. But, okay, I'm going to go back to what I was about to say, which is-
Yeah. What's that third one?
Third thing where we ultimately decided this was going too far, was actually adding code to make the tool happy and with understanding of why. An example of this is, in PHP there is a language provided function called assert. And you can say, "I assert this variable is an instance of," whatever. You can do that and at runtime, if it happens to not be, it would actually throw an exception, right?
Right.
So I had put those things in there to make PHPStan happy because PHPStan looks at those assertions, and it uses that to infer like, "Okay. Well if, past that line, I know that variable is definitely instance of or a subclass of," or whatever. And you're like, "What are you doing, Joel?" Why didn't you like that, Aaron?
Well, again, we're adding a bunch of functionality. I don't actually know how assert works underneath the hood, and I'm not a huge... I'm not worried about micro-optimizations per se. But it was tooling or coding that you were doing just for the tool. It wasn't that like, "Oh, I personally don't know sometimes if this is the wrong variable so I'm going to write defensive coding."
Sure.
It was, "I'm perfectly fine, everything's fine. I even have tests, they're passing. Now this tool doesn't like this so I'm going to use this extra bit of code now." So I've actually put code in for something that is measuring the quality of my code, it's a circle. Well, I was furious.
I know. Well, and to be fair, this was on sort of a... not a hobby project, but it was an experimental project where I had PHPStan all the way up to max, which we never do. So, I was just throwing things at the wall to see what would stick. But for me, I kind of knew when I did it like, "Ah, this isn't great." And the reason is, if we go back to this example of authenticatable versus user, let's just say, "I assert this is an instance of user," would that actually throw an exception, right? Now I've broken my code trying to make the tool happy. It's the complete opposite direction from why we're trying to use static analysis, why we're introducing types. It's like you're serving the tool and actually weakening the reason that you're using the tool. It just makes no sense.
I'm kind of a student of human nature, and I love watching people. Not like weird, sitting in a mall and watching people. Okay, so a mall is a place where there's a bunch of... Okay.
People know what a mall is, it hasn't been that long.
Okay. But I just like understanding how humans exist and think. You know that, I'm constantly asking questions and trying to understand how people think about stuff.
I feel like I want to ask, are you watching me right now? What are you learning from this experience?
No, it's not that. It's just the ways that we make decisions in our life are pretty interesting. For example, when you bond with humans. Think about growing up and how you bonded with friends and stuff for example. Or you just meet someone at a conference, you find out, "Well, his name's Aaron too. Best friends, clearly. Same names." I mean, are you like that? So if you met another Joel, you'd immediately be like, "Well, this guy's awesome."
I was thinking it could go either way. Best friend or like he's my nemesis now. Like, it can only be one of me in this group.
Oh, like a Highlander sort of thing.
That's right.
There can be only one Joel. Hey, Joel, do you know what my most favorite meal in the world is? It's mashed potatoes and beef tips, mmm.
That seems a little random, but I know what you're doing. You want to tell people about our new books of tips? We published three volumes. Just go to masteringlaravel.io, click on tips, and you can see where to download those books.
Delicious.