Planning a Command Runner0:00
So far, we've been building a lot of scripts, and to run them, we have to do something a little bit ugly, like php debug calls. And as we continue to add more and more scripts, that's going to feel a little bit hacky, a little bit unwieldy. In addition, these are just that, scripts. It would be really nice if we could have a uniform way of building these scripts to make it not only more straightforward to use them, but also easier to reuse them. Ideally, I'd like to be able to type something like shift, run, and then debug calls, but also be able to pass it more tasks, like lint, or format code, and so on. So, let's build a command runner. Now, we could, of course, do this ourselves, or continue to use low level components, like the Symfony console, but I think this is a great opportunity to use something like Laravel Zero. Laravel Zero is a subset of the Laravel framework, streamlined
Installing Laravel Zero0:55
level components, like the Symfony console, but I think this is a great opportunity to use something like Laravel Zero. Laravel Zero is a subset of the Laravel framework, streamlined for building console applications. It gives you all the same feel of Laravel, and as such, it should be pretty quick for us to get started and make a command. So, let's go ahead and install it, following their instructions, and start bringing this idea to life. So, we'll change this to shift-cli. Alright, and let's go on in to shift-cli. Now, it looks like they encourage us to rename this to be our own application name. So, we'll just copy this command again, and I'm just going to call it shift. Great. So, for now, we should be able to do something like php shift --help. There we go. We have the beginnings of our own little command line tool. So, let's go ahead and make a specific command, just
Creating the Run Command1:47
be able to do something like php shift --help. There we go. We have the beginnings of our own little command line tool. So, let's go ahead and make a specific command, just like we would in a Laravel application. So, we'll say php shift make command RunCommand. Alright, let's start customizing this command. I'm going to go ahead and kill some of these comments. We'll change the signature to be run, and this is going to accept a set of tasks. So, we'll say task, and I'm pretty sure if we put *, that means we can do multiple, and we'll give this a bit of a description for the help command. So, we'll say the name of the automated task. Cool, and again, we'll kill this comment. The description for the entire command will be run one, one, or more automated tasks. Great. Alright, let's keep going here. We'll continue to kill these comments. Let's come back to the handle command, and this won't run on a
will be run one, one, or more automated tasks. Great. Alright, let's keep going here. We'll continue to kill these comments. Let's come back to the handle method, and this won't run on a schedule. It's run on demand by the user. So, great. Okay, so in handle, just like any other Laravel command, here's where we're going to want to perform the command actions. So, in our case, we would loop over tasks and basically run them. So, what does that look like? Well, ideally a task, like we said, is going to be something like lint, and we would need to run them. In this case, kind of like phplinter.php. Now, we want to get rid of running things as an individual script. Again, I'd really like to move things to more of a class-based structure. Kind of difference between procedural programming versus object-oriented. We want all of that reusability. We want that organization. So, let's kind of pseudocode this out for what we want.
Building a Task Registry3:32
difference between procedural programming versus object-oriented. We want all of that reasability. We want that organization. So, let's kind of pseudocode this out for what we want. Again, some programming by wishful thinking, and then we'll go and change our scripts to fit this new structure. So, if I pass in kind of a short name and I want some kind of class name, then to me that means I need some kind of translation or lookup table or something of that sort. Now, having a database feels pretty heavy, so I think I'll make some kind of private function here for taskRegistry. Again, this may evolve over time, but it's good enough to get started. So, let's just make a quick mapping in here of our different tasks names. Again, the short name and then what the potential class name would be. So, here we can say something like CheckLint, and this would maybe go, again, simply to some kind of class like App\Tasks\CheckLint.
and then what the potential class name would be. So, here we can say something like CheckLint, and this would maybe go, again, simply to some kind of class like App\Tasks and then CheckLint. For now, let's just keep the ball rolling here. What else do we have? We have our debug calls, so we can do the same thing. App\Tasks, debug calls, class. All right, cool, and let's just copy this one more time and do that formatCode call, and this will be simply formatCode. Again, this just gives us a quick mapping of all the tasks that we have registered. I fully expect that we would add more over time. Okay, now with this registry, I think we can go back and bring this pseudocode to life. So, let's just foreach over each of the tasks that came in. So, $tasks, $argument, $task, as $task, and we could essentially pass that into our taskRegistry and get a class reference back. So, I could really shorthand this to say something like new $this->taskRegistry,
argument, task, as task, and we could essentially pass that into our taskRegistry and get a class reference back. So, I could really shorthand this to say something like new taskRegistry, and I could dereference that with task, and on that new object I could call something like perform to perform the task. All right, according to phpStorm, I might have been a little too wishful in my thinking. Let's go back and see what some of the issues might be here. Okay, taskRegistry dereferencing. I probably have a precedence issue here, so let's just separate that with parentheses. Okay, now most of this is gone. Okay, so taskRegistry returns an array of strings, and then we're going to dereference that based on the task. Now, this feels a little awkward to me that we're pretty much always returning a static reference from the taskRegistry and then dereferencing that. It might make a little more sense to pass in the task.
awkward to me that we're pretty much always returning a static reference from the task registry and then dereferencing that. It might make a little more sense to pass in the task instead of always returning the array. So, let's just tighten that up a bit and get rid of those parentheses. That just kind of feels a little ugly to me. So, let's pass in a string for the task, and instead of returning this array directly, we can just assign this to $tasks, and this is actually going to allow us to do some more runtime checks. So, now we can say something like if not isset($tasks[task]), then we could throw a new InvalidArgumentException and say task not registered. Pass in that task name, and finally return $tasks[task]. All right, we now need to change this to return a string instead of an array. All right, I'm not quite sure why phpStorm doesn't like that, but we'll
Implementing Task Classes7:38
and finally return Task. All right, we now need to change this to return a string instead of an array. All right, I'm not quite sure why phpStorm doesn't like that, but we'll figure it out when we try to run everything here in just a minute. Let's get rid of our pseudocode because I think we're pretty much done with our command now. We basically loop over any tasks that come in, and we attempt to call perform on them. Now that we've kind of driven out our API at a high level, let's go and bring some of these to life. So, I'm going to go ahead and create this class with phpStorm. We'll do app/Tasks and the file names CheckLint, where it's going to make the directory, the namespace, all that looks good. And we know inside of this class we need a public function perform, and so now I can copy all my code from those scripts into this perform method. So, with the power of some video editing, let me do that for the rest of these classes. All right,
function perform, and so now I can copy all my code from those scripts into this perform method. So, with the power of some video editing, let me do that for the rest of these classes. All right, in the process of filling everything in, I noticed two things. First, findFiles is actually referencing the global arguments being passed to the php script. We did this when we wanted to run things from our pre-commit hook to pass in the changed files from git. I think instead we could probably do something like adding a dirty option to our command to find those ourselves. So, I'm going to leave that optional for now and implement that a bit later. I think the easiest thing to do to keep moving forward is just to remove this, because it's no longer going to work within our own runner. Instead, just like before, we can loop over everything. It's not that big of a deal. It was really just an optimization to restrict it to just changed files. So, again, I'll add that
Testing and Handling Failures9:16
own runner. Instead, just like before, we can loop over everything. It's not that big of a deal. It was really just an optimization to restrict it to just changed files. So, again, I'll add that back later. The other thing I noticed is that some places were exiting directly, and I think within this new class-based framework, it makes a little more sense to instead return that value. Let me go ahead and make these same changes for that debugCallTask that I pulled over as well. So, let me get rid of this argv reference, and up here we'll change this to a return, and we no longer need to wrap that in parentheses. Okay, I think we're in a place where we should be able to try and run this. So, that shiftRunner is not currently executable, so let's just do php shift run check lint. Oh, looks like we have an issue with that taskRegistry. I remember phpStorm didn't like this for some reason. Let's go take a peek. My guess is the issue here, again, is precedence.
check lint. Oh, looks like we have an issue with that task registry. I remember phpStorm didn't like this for some reason. Let's go take a peek. My guess is the issue here, again, is precedence. That new command probably has a really high precedence, and what it's doing is it's actually trying to look at this portion of it as a property instead of as the entire method call. So, if we wrap this yet in more parentheses, we should be able to get around that issue. All right, let's try this again. php shift run check lint. Okay, it finished. I guess it didn't find any lint. I don't think I have any errors in my application. Let's run something a little more interesting, maybe something like format code, and it looks like it did. I notice in the code here this curly brace is actually on the same line. I must have had it on the next line, and if I scroll down here, yep, there's that notorious space of the not operator and that
code here this curly brace is actually on the same line. I must have had it on the next line, and if I scroll down here, yep, there's that notorious space of the not operator and that pesky no space between our string concatenation. So, it looks like I just need to pull over my custom pint configuration from our previous project, but functionally this is all running. We've wrapped everything within our own command runner using Laravel Zero. We've got a nice little registry here that we can expand upon to add more tasks and a nice object-oriented way to build tasks and run their actions. Before wrapping up, let's just make sure that we handle that result, and if that result is not equal to 0, then let's output an error. We'll say failed to run task, task, and we'll go ahead and return that result code from our handle method as well,
then let's output an error. We'll say failed to run task, task, and we'll go ahead and return that result code from our handle method as well, and finally we'll return 0 if everything was successful. So, just to make sure we didn't break anything, we'll run our format code again and we should see, yep, that string concatenation operator tightened up and everything still ran. Great!
