Rethinking revert design0:00
Okay, full disclosure, there has been a period of time that has passed between this episode and the previous episode, and that is because I just wasn't quite happy with how things were going. I've been trying to come up with a nice syntax for reverting steps, and everything I did seemed very hacky. I just knew if I pushed that up as a pull request, it was going to be closed, and I wouldn't blame Jess for doing so. She has to keep the code clean. It's her project. So I decided to rectify that problem, I was going to source dive prompts further.
It's her project. So I decided to rectify that problem, I was going to source dive prompts further. I spent more time building out different prompts and trying them out, seeing how it worked, going through the source code of the Prompt class line by line, how do key presses work, how does rendering work, and I also had a pairing session with Joe Tenenbaum, who is something of a wizard in the community when it comes to Laravel prompts. He spent a lot of time with it, he knows how the terminal works, and that session, it was just maybe an hour long, gave me so much insight that I think now I know where I want to go with this, and how to actually come out with a clean solution that is likely going to be merged by Jess.
Switching to while-loop1:15
with this, and how to actually come out with a clean solution that is likely going to be merged by Jess. So let's play around with that clean solution, and just keep in mind that often when you're building out these pull requests, you may need to reach out for help and feedback. Okay, let's go. As far as I see it, there are two different ways we need to handle this. One is, what if $revert is equal to false? What if you can't revert to the previous step? The other is, what if $revert is equal to a closure? You can revert to a previous step, and we need to execute that closure to perform any
The other is, what if revert is equal to a closure? You can revert to a previous step, and we need to execute that closure to perform any undo actions the user might have defined. In either case, I don't think a foreach loop is going to cut it, because we need full control over the current step that we're executing. So why don't we switch to a while loop? We'll have an integer called index that controls the current loop item, and we'll say, well, whilst ever the index is less than the count of steps, we want to loop through the steps, and we can perform this action here. So let's go ahead and do that.
and we can perform this action here. So let's go ahead and do that. Obviously, we don't have access to the step automatically, so why don't we destructure at the top of the while loop? We'll say stepRevertKey equals this step's passing in the index, and then in order to move to the next step after it's been executed, we'll have to manually increment the index. Okay, let's now get rid of the foreach, and let's execute this in the terminal. php playground steps. Already, I think it's working. Awesome.
Intercepting prompt keypress2:49
Already, I think it's working. Awesome. Okay, so with that working, we can think about how we would actually intercept a key press to signify that we want to revert the process. This is trickier than it seems, because once a prompt is executed, if it's in progress, there's no way to get in the middle of that and listen for key presses other than inside the prompt itself. Let me explain in code. If we jump into the Prompt class, remember this prompt method that we looked at a few episodes ago?
If we jump into the Prompt class, remember this prompt method that we looked at a few episodes ago? Let's scroll down to the while loop here. So basically, whilst ever there are keys being pressed in the terminal, we handle those key presses. You see the problem? We are in the StepBuilder, which essentially, if you think about it, has executed a prompt here, but then the prompt is stuck inside a while loop, and we have no way of being able to handle that inside the StepBuilder. So it handles key presses great, but we're too high up the food chain.
able to handle that inside the step builder. So it handles key presses great, but we're too high up the food chain. In other words, the only way I see us being able to actually do this is if the prompt has understanding of the revert process. So let's see if we can make that work. So we'll jump into handleKeyPress, and yeah, take a look at this. Note that handleKeyPress is already handling the cancel state. So if you click Control C, it will set the state to cancel, and it will stop execution. What if we add another if statement here? So if the key is equal to key, let's go with Control U for now, which I know is sort of
What if we add another if statement here? So if the key is equal to key, let's go with Control U for now, which I know is sort of like a negative affirmation in the terminal. We'll have to add that. I wonder if GitHub Copilot can help. Yeah, there we go. X15, that will do. I'm pretty sure that's right. And inside here, well, we can do whatever we want. For now, why don't we return false?
And inside here, well, we can do whatever we want. For now, why don't we return faults? We'll end the prompt early. Let's see if that works. So we'll go back to our demo. I'll restart the process, and I'll hit Control U. There we go. It's moving us on to the next step because we've not done anything with the while loop, but it is working. It's stopping the prompt in its current state.
Adding revert visual feedback4:59
but it is working. It's stopping the prompt in its current state. It would be nice to have some form of visual feedback that a revert has taken place. I wonder what the easiest way to do that is. What do we have available on the prompt itself? Let's take a quick look through. Ah, hold in, hold in, hold in. Okay, okay. Let me just try this. So if we start this again, and I'm going to put in a real name, but then for the path,
Let me just try this. So if we start this again, and I'm going to put in a real name, but then for the path, I'm not going to put anything in, or let's put something fake in like Laravel. Look at this. Please enter a relative path. That is, I'm pretty sure, the error state, and the reason I'm confident in saying that, take a look at the text renderer. The text prompt renderer has an error state, right? It renders a box, and then it renders a warning with the prompt's error, and if you take a look at that warning, note that that's a yellow text with this exclamation point in the message.
It renders a box, and then it renders a warning with the prompt's error, and if you take a look at that warning, note that that's a yellow text with this exclamation point in the message. So we could use that to give some user feedback on the fact that the step is being reverted. Let's just try this. I'm going to go into prompt again. We'll go down to control U, and let's say that we want to set an error, and we'll set that error equal to reverted, and I think we'll also have to set the state to error in order for this to show. So let's say this state equals error. Okay, let's see what happens.
So let's say this $state equals error. Okay, let's see what happens. We'll restart, and I'm going to click Control + U. Nice, that looks really clean. So now we have visual confirmation that the step was reverted, but you can see that the step wasn't actually reverted because it actually just moved on to the next step instead of going back to this step here. Let's fix that. We can't control the loop inside of prompt itself. We need to control the loop inside the step builder, and the only way I can think to talk
Hooking prompt to step builder6:45
We can't control the loop inside of prompt itself. We need to control the loop inside the step builder, and the only way I can think to talk from prompt to step builder is with some form of closure hook that we fire inside this if statement. So something like, well, if we have a property set called revertUsing, then we actually want to fire revertUsing, and we'll use call_user_func for that, and we can set revertUsing from the step builder. So let's add this property first. We'll come up to the top where we have all of the standard properties. Note that they already have cancelUsing, and I think they have validateUsing as well.
We'll come up to the top where we have all of the standard properties. Note that they already have cancelUsing, and I think they have validateUsing as well. So maybe here, for now, let's make it public for easy control. public static nullable closure called revertUsing, and then let's jump into the stepBuilder, and at the top of the while loop, perhaps after we've grabbed the correct step from the property, we could say prompt, and we'll set revertUsing to a closure. That closure is going to have access to the currentIndex, and we'll use an ampersand so that we can control it, and then it could just reduce the index by one. So index--. Will that work?
So index minus minus. Will that work? Let's try it. So we'll restart, and let's say Luke, and then I want to change it to Taylor. I'll hit Control U. Okay, this is interesting. So it did revert, but it's reverted to the same prompt. So we're not going back to what is your name, but we're re-showing where should we create your project. Why would that happen?
your project. Why would that happen? So this closure is executed after this. Ah, they're canceling each other out. Take a look. So we execute this step. When we hit Control U, this closure is executed, and it reduces the index by one, but then at the end, we increase the index by one again. So they cancel each other out, and we end up with a net zero result. So instead, why don't we introduce a property, wasReverted, and wasReverted can be equal
So they cancel each other out, and we end up with a net zero result. So instead, why don't we introduce a property, wasReverted, and wasReverted can be equal to false. We'll grab wasReverted in the closure, and we'll set wasReverted to true. Okay, so wasReverted equals true, and then down here, we'll say, well, if it wasReverted, so if the prompt wasReverted, we can go backwards in time, index--, otherwise, we can go forwards in time, index++, and that will give us full control over that flow. Let's see if that works. Restart.
Handling non-revertible steps9:25
Let's see if that works. Restart. Luke, I want to change it for Taylor, control U. Ha ha, look at that. So it says that we were reverted, and now I can type Taylor Otwell, and then it takes me to the next step again, where should we create your project. This is working pretty well. The next thing we need to handle is what happens when you said that you are not allowed to revert to a step. So in this case, we've dictated that you cannot revert after this intro, because it makes
revert to a step. So in this case, we've dictated that you cannot revert after this intro, because it makes no sense to see that intro again. What happens currently? So if I restart this, and I immediately hit control U, yeah, it's just rerunning again, whereas ideally, it would just come up with an error saying you cannot revert the step, and it wouldn't rerender anything to the terminal. So let's think about how that can be made possible. We could obviously introduce a ternary here. So we could say, can you actually revert, based on the closure or faults that comes
We could obviously introduce a ternary here. So we could say, can you actually revert, based on the closure or faults that comes from the steps property? Can you actually revert? If you can, then yes, we'll set this closure here, but otherwise, we're going to set revert using null. Okay. So now if we go into prompt, and let's go down to where we're making use of revert using. If we can revert using, then we call that user func. But if we can't revert using, then we could change the error message.
If we can revert using, then we call that user func. But if we can't revert using, then we could change the error message. So we could say, this cannot be reverted. And once we've set that error message, why don't we return true? Return true because you're not actually able to move to the next stage. And in fact, why don't we refactor this to be a bit cleaner? Let's say if not self revertUsing, then we just want to perform this action here. And that allows us to get rid of this if statement and clean things up a little bit. Very nice. Okay.
Very nice. Okay. Will that work? Let's find out. We'll go back into the terminal. Taylor Otwell. I'll hit control U. Okay. What is going on here? What is going on?
What is going on here? What is going on? Let's take another look. Go back to steps. I am an idiot. So basically, it makes sense that we declare whether you can or cannot revert a step at this point. I cannot revert this step once it's been executed. But if you think about it, the order is wrong in the step builder. In the step builder, the revert that we're actually checking is almost one step ahead.
But if you think about it, the order is wrong in the step builder. In the step builder, the revert that we're actually checking is almost one step ahead of itself. We need to change the order of this revert so that we have the previous revert available, not the current revert available. That'll probably make more sense in a moment. I think what we need to do is create some form of steps variable. And then let's loop over all of the steps in the property. So for each this->steps as step and we can grab the stepRevert and key. And then inside the loop, let's add that step.
So for each step as and we can grab the step revert and key. And then inside the loop, let's add that step. So steps brackets equals step revert key. But instead of passing this revert, I want to pass in the previousRevert. So why don't we track that here? previousRevert equals false by default because you can't revert the very first step ever. That doesn't make any sense. Then we set this property here to the previousRevert. And then finally, we set previousRevert to the current revert ready for the next step. I think that should work.
And then finally, we set previousRevert to the currentRevert ready for the next step. I think that should work. So now we just need to change any use of this.steps inside this closure here to actually make use of just the steps property that we've built. All right, let's see if that works. So we'll start this again. I'm going to click control U. Hey, look at that. So now, no, I cannot revert to this step here because as you can see inside my little steps example, revert is set to false.
So now, no, I cannot revert to this step here because as you can see inside my little steps example, revert is set to false. Let's make sure it wasn't a fluke. We also said that you're not allowed to revert after entering a password. So we'll go Taylor Upwell, path, password, and then I'm going to try and revert here. This cannot be reverted. That is awesome. But if I choose JavaScript and then I hit revert, you'll see it's reverted and I can go back to choosing between JavaScript and TypeScript. We are making progress.
Executing revert closures13:54
go back to choosing between JavaScript and TypeScript. We are making progress. What else do we need to implement? One of the other features we added was the ability to undo actions. So here, for example, we said that revert is a closure. And when you call revert on this step, it should execute this code here. Thinking about it, there won't actually be any possible point at which we could revert because the note will happen automatically and then the whole thing will end. So why don't we add just one more step in here, which is going to be a closure and maybe it could just run confirm and it's going to say finish installation and that will allow
So why don't we add just one more step in here, which is going to be a closure and maybe it could just run confirm and it's going to say finish installation and that will allow us to actually go back to this step here. Okay. So can we implement the revert closure? We'll go back into the step builder, come down to the while loop and we have wasReverted. If we did revert, well, then we need to execute that closure, don't we? Which is up here. So let's go ahead and try that. We say index minus minus, but before we say index minus minus, let's say call_user_func
So let's go ahead and try that. We say index --, but before we say index --, let's say call user func and we'll pass in revert. Will that do it? Let's go back to the terminal, rerun, taylor upwell, path, password, TypeScript. We'll install the dependencies. The spinner appears. And then when I revert this, it should uninstall the dependencies. Nice. So it is uninstalling.
Passing responses into revert15:23
Nice. So it is uninstalling. And then we go back to the install dependency step. One thing that's just made me think, if I click no here and then I revert, it's still going to uninstall, but that doesn't make any sense. I didn't install dependency. So why would I need to uninstall dependencies? In other words, I think when we define steps, we should receive responses just as we do, say here, when we add a step, we should receive any responses. And by doing so, I'd be able to say, well, actually, we never installed.
say here, when we add a step, we should receive any responses. And by doing so, I'd be able to say, well, actually, we never installed. So I don't need to uninstall. So I'll accept responses in here. I'll also add a key here and let's say it install. And then we could wrap this whole thing and say, only if responses install, do I want to spin and show the uninstalling prompt? Right. In order to actually make that work, I think we would just have to pass this responses into call_user_func here.
In order to actually make that work, I think we would just have to pass this responses into call_user_func here. And in fact, we could use more modern syntax. I could just do this, revert with this responses being passed in. Let's see if that works. So once again, Taylor Otwell, path, password, TypeScript, those are fine. We're not going to install dependencies. Then I realized, oh, I should have installed dependencies. So I'll hit Control U. Nothing happens.
Planning tests and cleanup16:45
So I'll hit Control U. Nothing happens. But now if I install dependencies, dependencies are being installed. Now I revert, boom, they are being uninstalled. This is brilliant. There's certainly plenty of work still to do. We've got cleanup to do. We need to write tests. In fact, you know what? Let's get a handle on how tests are written in Laravel prompts.
In fact, you know what? Let's get a handle on how tests are written in Laravel prompts. And then we'll back up what we've just created with some solid tests. See you in the next episode.
