Introducing PHP linting0:30
Heyo, I'm Jason McCreery. Those of you in the Laravel community know me as JMac, the creator of Shift. And in this series, I'm going to show you how to automate some of your everyday development tasks. Before we can write any kind of automation, we need to make sure that the php code is valid. That means it's free from any kind of syntax errors. It's actually kind of hard to write some syntax errors on purpose, but let's say we did something like this. Now there's already some packages out there that can check your project for syntax errors.
like this. Now there's already some packages out there that can check your project for syntax errors. Most notably, this phplint package. But php itself actually has a built-in way to do this. Let's take a look at this option with php --help. And if we look here, there's actually a --l option for lint, which runs a syntax check only. So let's use this option to scan our linter, which we know has a syntax error. And there we go. We get an unexpected identifier s on line 3, because we didn't escape our apostrophe. So this can actually work for us, and it's already built into PHP.
Handling multiple files1:41
We get an unexpected identifier s on line 3, because we didn't escape our apostrophe. So this can actually work for us, and it's already built into PHP. We don't have to worry about downloading any other packages or having any other dependencies. But there's a caveat with this. If we copy the linter file to some other file that's going to contain the same syntax error, and we try to run php -l on the linter, as well as some other file, we'll see that it only parses the linter. It doesn't actually parse some other file. Or said another way, you can't pass this multiple files. So we'll need to actually develop a way around this.
Finding PHP files2:15
Or said another way, you can't pass this multiple files. So we'll need to actually develop a way around this. So what we can do in our linter script is find all *.php files, and then pass them to php using the --l option. That's basically all we need to do. Pretty straightforward script. The tricky part is finding all *.php files. Well, that's actually not that bad, because Symfony has a Finder component. In fact, this page has some examples for doing exactly what we need. So let's go ahead and install the Symfony Finder.
In fact, this page has some examples for doing exactly what we need. So let's go ahead and install the Symfony Finder. We'll just copy their little command here. Let's go back and take their example usage. And we'll need to make sure that we actually require vendor/autoload.php, because right now this is just a basic php script. So let's see what this is doing. It creates a new Finder, and then it finds all files in the current directory. That's great, but we know that that's a little bit aggressive. We don't want all files.
That's great, but we know that that's a little bit aggressive. We don't want all files. We just want PHP files. So let's find a way to scan that. Jumping back out to the documentation page, if we scroll back up, let's search by filename. There we go. And like I said, the top example here is a nice way to limit this to just files with a .php extension. All right, let's add this to our Finder. So in directory, find files, and let's just split this up a little bit to make it a little
All right, let's add this to our Finder. So in directory, find files, and let's just split this up a little bit to make it a little more readable. So we're going to find all files in the current directory and make sure that they have a .php extension. All right, that should be a good start for the Finder. Check if there are any results. Let's just turn this into the opposite. So we'll say if there aren't any results, let's return, but we're actually a script here.
So we'll say if there aren't any results, let's return, but we're actually a script here. So let's say exit. That's fine. So what we'll do is we'll loop over each of the files found in the Finder, and for now we can just echo this absolute path. That's fine with me. Okay, let's see how this looks. If we run php linter, oh wow, that's a lot of noise. Let's clean that up.
If we run php linter, oh wow, that's a lot of noise. Let's clean that up. Let's change that, give it a little line break here, run this one more time. Okay, definitely getting a lot more files than we want, and that's because we can see right away it's looking in the Vendor folder and getting all those Symfony and Composer files instead of just the files in our project. So let's jump back up here, and let's make sure that we're excluding the Vendor folder. So we can use exclude, and it looks like we can pass this an array of directories or a string, so in this case I only care about the Vendor folder. So we'll just put that there, save this, go back, and run it again.
Running lint per file4:54
string, so in this case I only care about the Vendor folder. So we'll just put that there, save this, go back, and run it again. And now we get just our two php files. So while this might need some more tweaking as we grow our project, this is a good enough start. So if we jump back to our script, instead of echoing this file out, let's go ahead and run a system command, and in this case we'll run php -l and then pass it that absolute path name. Okay, now system behaves a little bit differently than some of the other php functions you can use to execute system commands.
Okay, now the system behaves a little bit differently than some of the other php functions you can use to execute system commands. So let's take a peek at this. If we take a look here, the problem I have with this one is that it kind of automatically gives you the last line of the output, and you don't have a whole lot of control to kind of capture that. If we take a look at something like exec, we'll actually get a little more control where we can pass in the output, and we can also capture that result code or exit code. And those are going to be important any time you write scripts that interact with the underlying system.
And those are going to be important any time you write scripts that interact with the underlying system. So let's change this to exec, and let's make sure that we're going to capture the output and the exit code. And since we're in a loop, it's also going to be a good idea to go ahead and default these or reset them for each iteration. So let's reset that to an empty array, and let's make sure we reset exitCode to zero. If I was only running this once, we could allow php to dynamically set those, but again, since we're in a loop, you want to make sure to reset them, especially for exec. If we go up and take a look at the output, we'll see that exec will append to the end
since we're in a loop, you want to make sure to reset them, especially for exec. If we go up and take a look at the output, we'll see that exec will append to the end of the array. So again, definitely a good idea to reset that. And finally, what we'll do here is let's just var_dump the exit code, and we'll also print out that output. All right, let's run our linter script again. And we see here that we have that unexpected identifier. We got an output of 255, and we had no syntax errors in our linter, of course, that we're running.
We got an output of 255, and we had no syntax errors in our linter, of course, that we're running. So we got a zero on the exit code. Now from a system perspective, zero or no code means that there was no error. Anything else relates to some kind of error code that theoretically you can go look up. Now, I'm not going to focus too much on the output of this right now. The main thing I want to point out is that notice that we are getting some output here. That's not in our array. Let's temporarily disable those and run our linter again. And this is kind of what I wanted to point out.
Redirecting stderr output7:27
Let's temporarily disable those and run our linter again. And this is kind of what I wanted to point out. Even though we're capturing all of the output, we're still getting some of the output in our terminal. And that's because it's being output to standard error instead of standard output. So we need to redirect that to make sure it all goes to the same place. And we can do that by adding a little bit of what some might consider line noise to the end of our script. So we can redirect a file descriptor of 2, meaning standard error, and send that to 1. And if we run this again, we shouldn't see anything.
So we can redirect a file descriptor of two, meaning standard error, and send that to one. And if we run this again, we shouldn't see anything. And just real quick, let me bring up print_r again and notice that instead of just the three elements that it had, it now has four with the top one being that message that was getting output to standard error. So this is ensuring that all of the output is going to our array and we can parse that in the next video. So let's turn this off. And for now, all I really care about is if I have any lint in my project. So if we run this again, phplinter, we'll see that it pretty much does nothing.
And for now, all I really care about is if I have any lint in my project. So if we run this again, phplinter, we'll see that it pretty much does nothing. If we run php with the --lint option against some other file, which still has our error, notice something in my terminal. I actually get an X here, and that's because I've set up my terminal prompt to let me know any time the previous command failed. And if so, I get this little X. So this all relates to that exit code. What we need to do is make sure that our linter is outputting a non-zero exit code if we do have lint.
Returning proper exit codes9:02
What we need to do is make sure that our linter is outputting a non-zero exit code if we do have lint. So let's set that up. We can track that within our loop. We'll basically say failure equals false. And inside here, we can really just say if exitCode is not equal to 0, then make sure that we know there was a failure. Right now, we don't care about how many failures or anything like that. We really just need to know that there was lint. And to make sure that this script fails, we can exit.
We really just need to know that there was lint. And to make sure that this script fails, we can exit. And in this case, if there was a failure, we'll exit with a non-zero status code, otherwise just zero. And now if we run our linter, and now we'll see that it changes my prompt just like php -l option. Let's go fix that some other file, and we'll properly escape that to remove the syntax error. And when we run this again, now we'll see that my prompt is normal with just the little dash.
And when we run this again, now we'll see that my prompt is normal with just the little dash. And in this case, that means that our project doesn't have any lint, and our linter is working like php -l option, but it's scanning all files within our project.
