Reducing Controller Arguments0:00
Hey there, and welcome back. On the last lesson, we spoke about cleaning up things a little bit, and this is what we're going to do now. Let's start with the ChecklistController and see what we're passing to our dependencies. We only have one, which is the PurchaseItems class, and we're passing quite a few things. We're passing the cart items, we're passing payment information, and we're also passing user information. Right now, we have five arguments. We could narrow it down to three. We could grip this within an object, and we could grip this within an object as well.
Creating PendingPayment DTO0:30
We could narrow it down to three. We could grip this within an object, and we could grip this within an object as well. As usual, let's run our tasks to make sure that things are working. They are, and now we're good to refactor. Let's start this by creating an object to encapsulate payment data. Let's call it PendingPayment, and we're going to create a new PendingPayment object. This doesn't exist yet, so let's create it. We can put it on the order module. Let's put it under DTOs, for now at least, and this is going to accept two things. First, it is going to accept PayBuddy.
Let's put it under DTOs, for now at least, and this is going to accept two things. First, it is going to accept PayBuddy. We're going to abstract this later, but for now, this is good enough. We're going to call it the provider, and it's also going to accept a paymentToken. Let's make this class read-only. It's meant to be mutable, and now let's go back to our CheckoutController. Let's instantiate PayBuddy and also grab the paymentToken. There we go. The second thing is the userData, and we can have that on a DTO. We can say UserDTO, just like we did with the ProductDTO.
Introducing UserDTO Module1:31
The second thing is the user data, and we can have that on a DTO. We can say UserDTO, just like we did with the product DTO. Let's call a function called fromAlloquinModel, and then we can pass the user. Now, we also have to create this class. We don't have a User module yet, so let's create one for now that's only going to have that DTO. It's going to be called ModulesUser. There we go. We need to create this method. User is most definitely not mixed, so let's expect the user model.
We need to create this method. User is most definitely not mixed, so let's expect the User model. This is going to return an instance of itself, and then we can add some properties here. Let's construct this, and first, we need the ID. That's pretty obvious, so ID. We also need the email, and we also need the name. This class is read-only, and now we can return it. Now, the good thing is, since we're generating this on the outer layer of the controller, if we ever need more information,
Now, the good thing is, since we're generating this on the outer layer of the controller, if we ever need more information, we just need to add a property here, and since we have this method that generates data from a User, AlloquinModel, we can also do this here. Okay, let's refactor this class, rather, this handle method to accept this, so it is going to accept a bettingPayment, and it is also going to accept the userDTO. Let's get rid of all of this, and we can also align this, right? First, let's change the signature. Let's go to PurchaseItems, Handle.
First, let's change the signature. Let's go to PurchaseItems, Handle. Now, we want to accept. Let's get rid of all of this first, so we want to accept a pending payment. Let's import the class, and a UserDTO, which we're going to call user. There we go. Now, let's run our tasks. They're going to fail. That's expected.
They're going to fail. That's expected. Let's see the failure. What was it? Undefined variable paymentToken. That's expected, so let's refactor this. We don't need any of this, especially because we don't have those. We have a User object, so let's use it to start the order. We have to add it to the closure. We have the items as well.
We have to add it to the closure. We have the items as well. The ID is within the user object. The payment provider is within the pending payment object. Let's import this, and the token is also within the pending payment object. All right, we also have to update things a little bit down further, user object, and user email. Let's run our tasks and see what happens. Okay, they're passing, so we've cleaned this up a little bit, and we could probably line this as well.
Okay, they're passing, so we've cleaned this up a little bit, and we could probably line this as well. It would still look good. I'm not going to do it. I prefer to have them in separate variables. Let's just remove this line break. So this is pretty interesting. Now we have the controller providing data to send to an inner layer. Think of this as another layer, which is where it receives an HTTP request. We're sending that to an inner layer that actually handle the business rules,
Refactoring OrderFulfilled Event4:34
Think of this as another layer, which is where it receives an HTTP request. We're sending that to an inner layer that actually handles the business rules, and then we're just sending a response back. All right, now let's look into our event, which is also very bloated. We have three things related to an order here. We could also encapsulate that within one object. Let's go into the OrderFulfilled event, and let's rethink this a little bit. So we can start with an OrderDTO, which doesn't exist yet. So let's say OrderDTO, and then we can get rid of all of this. Well, although the creditItemCollection works here,
So let's say OrderDTO, and then we can get rid of all of this. Well, although the CreditItemCollection works here, that's not actually what we want. We actually want an array of orderLines. So let's replace this with an array of orderLines, and then we want a UserDTO as well, since we want user data. Let's create this OrderDTO. We already have a DTO's module here. I'm sorry, a DTO's directory. So for the OrderDTO, what information did we need again?
I'm sorry, a DTO's directory. So for the OrderDTO, what information did we need again? Well, let's go with, we need an id. We need the total in cents, and we also need the localizedTotal, at least for now. And this is a string. This is a read-only class as usual. Data transfer objects should always be read-only. And as usual, let's create our different Eloquent model static function. We expect another model.
And as usual, let's create our different Eloquent model static function. We expect another model. Let's import this class, and we can return a new object. So the ID is the orderID. Well, since we have only a few arguments, let's just inline this. ID, total in cents, and localizedTotal. There we go. Okay, so we have the orderDTO. We have an array of orderLines,
Okay, so we have the OrderDTO. We have an array of OrderLines, and we can either use an OrderLine collection, or we can use a doc block to tell Static Analysis that this is an array of OrderLine. We don't have an OrderLine DTO yet, so let's create it. This is going under DTOs. And now phpStan and phpStorm are going to know that we're talking about an array of OrderLines here. You could also write this like this.
that we're talking about an array of order lines here. You could also write this like this. Same thing. We don't need the fully qualified namespace here, so let's just get rid of this so they look the same. Okay, cool. If we run a test, they're still going to fail because even though we've changed the signature, we haven't changed anything else. So let's go here, and let's rewrite this.
we haven't changed anything else. So let's go here, and let's rewrite this. First, we're expecting the Order. Then we expect the OrderLines, and this property does not exist yet, and then the User. Let's get rid of all of this. If we run a test, they should still fail. Yep, we're expecting an OrderDTO, and we're receiving an Order Model. So let's change the return of this function right here.
and we're receiving an Order model. So let's change the return of this function right here to actually return the DTO from the Order. So OrderDTO, let's import this from Eloquent Model, and then we can pass the order here. We also need to update this doc block. We're going to update the return type of this. And there we go. Now, we're looking for this lines property, which doesn't exist, but this could be a nested object.
Now, we're looking for this lines property, which doesn't exist, but this could be a nested object. So we can go into the OrderDTO, and it doesn't make sense to have an Order without its lines. So let's add this right here. We want to have an array of OrderLines. Let's call it lines. And let's also add a doc block to say that this should be an array of OrderLines,
And let's also add a doc block to say that this should be an array of order lines, just like we did on the other one. So we're instantiating this here. We need to pass the order lines. We need to get an array. Let's go to the OrderLine DTO. First, we can remove this since we've found out that we have all the information that we need encapsulated on the OrderDTO.
that we have all the information that we need encapsulated on the OrderDTO. Let's go into the OrderLineDTO, and now we're going to start with a constructor. Now, we're not really interested in its id because it's a part of an Order. It doesn't really make sense to have that alone. You're not going to fetch an OrderLine by itself. Now, we are interested in the productId. We are interested in the productPrice in cents,
Now, we are interested in the productId. We are interested in the productPrice in cents, and we are interested in the quantity. Let's add our usual from Eloquent model method, expect an OrderLine, and we return an instance of itself. Okay, I noticed a typo right here. It's not productPrints. I love prints, but it's not prints, it's price. And now we're also going to add a new method.
I love prints, but it's not prints, it's price. And now we're also going to add a new method to return an array of orderLines, which we can call from Eloquent collection, and we could expect to receive a collection here of orderLines. We're going to return an array, and since we're receiving a collection, we can call it from Eloquent collection. We can call collection methods, can return orderLines.
we can call it from Eloquent collection. We can call collection methods, can return order lines. We can map this, since we're going to have an order line here, we can map this into an Eloquent model, and then we can return an array from this. All right, let's see if this works. Within our order DTO, we still have to pass the lines. So first let's format this to make it easier to read, and then let's call order line DTO
So first let's format this to make it easier to read, and then let's call orderBy on the DTO from Eloquent collection, and then we can pass our collection of lines. As you can see, it returns a collection. It actually returns an Eloquent collection, so we can type hint this and be even more specific on the type. Let's go with an Eloquent collection. All right, let's run a test and see what happens.
Let's go with an Eloquent collection. All right, let's run a test and see what happens. Still failing. Order fulfilled, argument two should be userDTO, and an array was given. Let's see what's happening. We don't need these lines, because we already have that within the order, and if we dump and die this orderDTO real quick, let's see what we have.
and if we dump and die this OrderDTO real quick, let's see what we have. Yeah, that's pretty much what we need. We have an array of lines with the OrderLineDTO, and we have the base DTO with its information. Perfect. Let's go back into our event. That looks good as well. We have an OrderDTO and a UserDTO. We don't need this doc block anymore,
Updating Listeners for DTOs10:28
We have an OrderDTO and a UserDTO. We don't need this doc block anymore, so let's get rid of it. Let's run a test. Okay, now we're getting undefined property, and it's on the sendOrderConfirmationEmail handler. So let's go there to this listener, and let's refactor this. Since now we have access to this UserDTO, we can simply call the UserDTO and the email,
Since now we have access to this UserDto, we can simply call the UserDto and the email, and since we also have access to the order, we can pass your localized total. Let's rerun a test. Okay, now we have a problem on the decreased product stock. Let's go into that listener and do the same thing. So let's rewrite this. First, we have access to the order, and then we have access to its lines.
First, we have access to the order, and then we have access to its lines. Let's leave for each here, and now we can call the product stockManager and decrement the... Again, we have access to the orderLine, the product, and this looks wrong. So, huh, this is weird. We have an OrderDTO. Oh, we're type hinting the wrong thing here.
We have an OrderDTO. Oh, we're type hinting the wrong thing here. So phpStorm wasn't understanding it correctly. Let's type hint it correctly. We can get rid of this. It's not an OrderLine model, but an OrderLineDTO, which is a read-only DTO. Let's make sure we have read-only here as well, and let's go back to what we were doing. So, my bad, wrong dot block.
and let's go back to what we were doing. So, my bad, wrong .env block. Inside the order line, we have access to the productId, and then we also have access to the quantity that was purchased. And now we can get rid of this block and rerun our tests. Still failing. Let's see what's up. Okay, we don't have access to the orderURL, so that's an easy one.
Okay, we don't have access to the order URL, so that's an easy one. We just have to add it. Let's add a property called URL, and then we can pass it here. Let's rerun our tests. We still have something failing. Call to undefined method, orderDTO URL. Okay, that's not a method anymore. That is a property.
Okay, that's not a method anymore. That is a property. So let's go into the CheckoutController and replace this with a property. Perfect. Now we have our test passing. We've encapsulated most of our data within data transfer objects. We are not leaking Eloquent models anymore, and we have used events to solve temporal coupling.
Why Use DTOs12:33
We are not leaking Eloquent models anymore, and we have used events to solve temporal coupling within our purchase items action. Now, this looks rather simple. We can even align this. There we go. Now, you might be asking, isn't this a little bit overkill? Why do we have to create those data transfer objects if we already have access to the model?
the best approach for you. However, in an application built with Laravel, sometimes creating those Data Transfer Objects can help keep your code clean and organized. When you're dealing with a User model, for example, using a UserDTO can isolate the data that needs to be passed around, making it easier to manage and understand.
So again, context matters. What works for me is not necessarily what will work for you, and what works on project A is not necessarily what will work on project B. With that said, the benefit of having those data transfer objects is that now we have a middleman between the modules. We have specific objects that are designed to be passed through different layers.
We have specific objects that are designed to be passed through different layers. If we were to change the internal implementation of any of our models, as long as the data transfer object was stable, the other modules wouldn't have to worry about it. Since we're not leaking any models and those are read-only, remember, you can mutate them, you can't do much with them,
remember, you can mutate them, you can't do much with them, they don't have behavior. The amount of actions each object in our system can do with them is very limited, which again, as I said in the previous episodes, decreases the surface area for mistakes. We're going to talk a little bit more about data transfer objects and how coupling matters in the grand scheme in the future,
about data transfer objects and how coupling matters in the grand scheme in the future, but a phrase that I always use is, coupling is not binary, it is a spectrum. It's not that you're coupled or that you're loosely coupled. It's a little bit more nuanced. You have a spectrum. And I say this all the time, but context matters. What works for one feature
but context matters. What works for one feature might not work for another. You might have, let's say you have very simple code features. Using models are fine. Using output models is completely fine. If you have more complex features and you're worried about coupling and how they interact between different modules,
and you're worried about coupling and how they interact between different modules, then it's probably interesting to have those middlemen. Anyway, this is enough for this lesson. As you can see, we have a much more understandable object. It accepts less parameters. They're pretty clear on what they hold and what their purpose is. And overall, this is just much easier to understand. All right, let's wrap up this lesson.
And overall, this is just much easier to understand. All right, let's wrap up this lesson. I hope you guys enjoyed it and I see you on the next one. Bye-bye.
