تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Refactoring Module Boundaries0:00

Hey there, welcome back. On the last lesson, this is where we finish things off. We have this fairly big controller, which handles a checkout flow. And I mentioned that we were going to refactor this, and most importantly, focus on the cross-boundary communication that is. We are on the Order module, but we are also communicating with the Payment module, as well as the Product module. In a modular monolith, one of the goals is to ensure that you have some level of separation between modules that allows you to safely work on those modules individually without affecting the rest of the system.

between modules that allows you to safely work on those modules individually without affecting the rest of the system. In this example, we are directly interacting with core entities of other modules. For example, we are interacting with a Product, we're also interacting with a PaymentProvider. So think with me, what if we were to change something internally within this module? It would break the checkout. And the same thing happens when we talk to the Payment module. If we were to change something internally, it could break the CheckoutController, which, although uses the Payment module to obviously charge credit cards, should not have been affected.

although uses the payment module to obviously charge credit cards, should not have been affected. Interacting with those modules gives you a lot of confidence and also a great deal of autonomy when working with them. It allows you to safely change the internals of each module to better suit whatever pieces need to happen and not affect other modules. And this is what we're going to focus on in this lesson. First, we're not going to change this task because it is an integration task and it does exactly what it needs to do. Our goal is to ensure that this keeps working.

Introducing CartItem DTO1:43

exactly what it needs to do. Our goal is to ensure that this keeps working. So we're going to be rerunning those tasks all the time. Let's go back to our CheckoutController and see what we can do here. The first thing that I want to do here is to get rid of this array. You see, we could use an object that properly encapsulates a Product and also a Quantity. That would be, for example, a CartItem. So let's see how we can do that. So instead of returning an array here, we can return a CartItem that accepts a Product and a Quantity.

So instead of returning an array here, we can return a CartItem that accepts a Product and a quantity. So let's do something like product, product details. We want to find the product. And the second argument is going to be the quantity. Like that. And now we can get, whoops, rid of this. We have to create this class. Now I want to create this class within the Product module since it is related to products. I don't know exactly where it fits.

Now I want to create this class within the Product module since it is related to products. I don't know exactly where it fits. This is a data transfer object, if you will. We're going to refactor this as well, but it's basically an object that's meant to be passed through different layers and modules. So for now, let's leave this at the root of the module. We want to fix the types, obviously. So let me get rid of this entirely and start from scratch. Let's go with the read-only class because we're not going to mutate this class. And I don't know why php thinks I'm on PHP 8.1.

Let's go with the read-only class because we're not going to mutate this class. And I don't know why php thinks I'm on PHP 8.1. Let's update this to 8.2. There we go. So we want a read-only class because we're not going to mutate it. And we're going to send a constructor. The first thing to accept here is a product and this is supposed to be public. We want to import this class and also a quantity. So have a quantity argument as well. All right, perfect.

So have a quantity argument as well. All right, perfect. Now we can type in this so that PHPStorm understands it. We can say that this is a collection made of CartItem objects. As you can see, PHPStorm already understands this, but your code editor might not. So I'm going to leave this right here for now. So for example, if I were to do a foreach here, PHPStorm is going to pick up on its properties. Now, the second thing is the orderTotal and cents. We would have to refactor this a little bit, especially because this now is a CartItem.

Now, the second thing is the order total and sense. We would have to refactor this a little bit, especially because this now is a cartItem. And then we have, it's not an array. So let's replace this with cartItem and call the quantity property and the same thing here. This is cartItem and we have a product. So let's run a task to make sure this still works. One of them is failing. And I use object of type cartItem as an array line 45. Okay, we have to update this as well because this is no longer an array and products should not be called products anymore.

Okay, we have to update this as well because this is no longer an array and products should not be called products anymore. Those should be cart items. So let's go with cart items. There we go. We're going to have to replace this as well, especially because this shouldn't be called product. This should be called cart item as well. There we go. This is not an array.

There we go. This is not an array. This is an object. Okay, you don't have to watch me do each one of those. It's already done. Let's rerun the task. We're back to green. Okay, perfect. The second thing is we're doing operations within a collection of cart items. We don't have to do this manually.

Creating CartItemCollection5:04

The second thing is we're doing operations within a collection of cart items. We don't have to do this manually. In fact, we should probably have a CartItemCollection that allows us to encapsulate that logic. So let's go to Product. Just like we have a CartItem. Let's create a CartItemCollection, CartItemCollection. We don't want to extend anything. Let's construct something here. And here we're going to accept a collection of items.

Let's construct something here. And here we're going to accept a collection of items. And we can tell phpstorm in static analysis shows that this is going to be composed of cart items. Perfect. Since we're mapping this from a request, we can also create a static method to be helper. Something called fromCheckoutData, for example, where we expect some data. And this is going to return an instance of CartItemCollection, where they have this map right here. So I'm going to refactor this and say that cartItems is going to be a CartItemCollection.

map right here. So I'm going to refactor this and say that cartItems is going to be a CartItemCollection from checkoutData. And fast, what do we get within the request? There we go. So let's copy this so that we can reimplement this on this class. We have to replace this with what we have on the argument and we can get rid of the doc block. And we can simply return a new instance of this class with the cartItems. Okay, now that we have this instance of the CartItemCollection, we can also encapsulate

And we can simply return a new instance of this class with the cart items. Okay, now that we have this instance of the cart item collection, we can also encapsulate this total within the collection. So let's copy this and say that we want the total and sets. We need to create this method. And we can just return this right here. We have renamed this to items. So we've just accepted collection of items on the constructor. And then we can create methods that do things with those items. In this case, this is what we want to do.

And then we can create methods that do things with those items. In this case, this is what we want to do. Let's run our task. Okay, now we're failing. Let's see what's happening. Line 62 on the task 52 sorry. We're not creating borderlines. Let's see what we got going on. Okay, the problem is the cartItems is not iterable. So you cannot iterate on it.

Okay, the problem is the cartItems is not iterable. So you cannot iterate on it. A simple thing is we can create an items method. We also have to implement this. And we can just return the items which is a collection. As usual, you want to type in this I'm sorry to add a docblock to tell psy analysis and peachstorm that this is composed of cartItems. Let's rerun a test. We are back to green. So at this point, we're now calling a different module, which is the Product module where

We are back to green. So at this point, we're now calling a different module, which is the product module where the cart item lives and where the cart item collection lives. We were doing that in a very organized manner. Now, one thing though, is we are still licking the product element module. I'm not saying this should be a role. And I'm not saying that you should be super afraid to lick Eloquent modules all the time, especially because this is a large application and Eloquent is one of its most powerful features. But bear in mind that since we're leaking that we're leaking something that belongs to different module.

But bear in mind that since we're leaking that we're leaking something that belongs to different module. We're also looking at the implementation. So right here, where you have access to the Product, you could do bad things, you could do things that are not related to this module, you're just increasing your surface area to do unwanted things. We also have to be careful with changing the Product's internal implementation as well, because you have different modules calling it directly. So let's see how we can get rid of this. Well, this Product model that we're leaking.

Replacing Model with IDs8:47

So let's see how we can get rid of this. Well, this Product model that we're leaking. The first thing is we want to remove this from the CartItem. Let's go to the CartItem class. And let's replace this with a productId. Now this is a proper detail, we only have primitives here, we have a productId and the quantity, that's all. Let's get rid of this usage here. And now we have to refactor this, right? We do not want to find the Product anymore.

And now we have to refactor this, right? We do not want to find the Product anymore. We just want to pass the ID. If we run a test, it is surely going to fail because we don't have a model anymore. If you run it, well, it fails, we know it is going to fail. Now one thing though is we calculate the total for this cart, right? But we're just returning the ID. So what can we do to also return all the information we need to perform these operations? And what we can do is we can create a data transfer object meant for the Product. For example, we could go here, we are inside Product, right?

Adding ProductDTO Abstraction9:46

And what we can do is we can create a DataTransferObject meant for the Product. For example, we could go here, we are inside Product, right? I'm going to create a new class called ProductDTO. It is going to also only have primitives. So we want things like we want the id, we want the price in cents. And I think that's pretty much it, right? We probably also want the stock. Let's call it unitsInStock. And then we can create a site method called fromEloquentModel. So we're working with Eloquent models.

And then we can create a site method called from Eloquent model. So we're working with Eloquent models. We expect a Product, we return a ProductDTO as well. We also want this class to be immutable. So we want it to be read-only. We're not planning to change anything here. And now we're going to return a new ProductDTO using what we have from the model. So the id is the product's id, the priceInCents, product priceInCents, and the unitsInStock. Now, you might be thinking, okay, this is really, really, really dumb.

units in stock. Now, you might be thinking, okay, this is really, really, really dumb. You already have the model, you already have the properties on the model. Why are you creating another class that's only boilerplate? And the reason is very simple. This class is meant to be used when you're interacting with different modules. And with that, we can hide our internal implementation of the product and have it live on this DTO. So the other modules are not going to be aware of the product model itself. We have freedom to change whatever we want on the model. And then the only place where we have to update things is on the DTO because it's all that's

We have freedom to change whatever we want on the model. And then the only place where we have to update things is on the DTO because it's all that's relevant for other modules to communicate with it. Let's go back to our current item collection. And now we want to return the DTO for this product. So let's use the set method we just created ProductDTO from Eloquent model. Let me go ahead and format this a little bit. We want to find a product like this. Obviously, we also have to update the correct item to expect a ProductDTO here. And now we have to update the total incentives.

Obviously, we also have to update the correct item to expect a ProductDTO here. And now we have to update the total incentives. Now the good thing is we have everything is as good as we can type wise. So we have access to all of that. We have a product which is a ProductDTO and then we have the pricingSense property. All right, let's rerun the test. It's still going to fail. We got a 500 call to undefined method ProductDTO::decrement obviously the method does not exist. We also did not want to add it here.

Extracting Stock Management12:20

exist. We also did not want to add it here. So think with me, this is a good opportunity to introduce something new. If you have an ecommerce store, you also have to manage stock, which means that you have some warehouse functionality. And this is a perfect place for a warehouse module to live in. Maybe a sub module of the product module. As usual, I don't want this lesson to be huge. So let's go ahead and create a directory here on the product module. And as you can see, now we start to differ from the usual group by type level structure.

So let's go ahead and create a director here on the Product module. And as you can see, now we start to differ from the usual group by type level structure. We have a sub module here and we're going to create a class called ProductStockManager. We only want to decrement things now. So let's create a method called decrement. And we expect a productId and a quantity. So how many units we want to decrement, it's going to return void, we don't want to return anything here. And now since this is inside the Product module, I'm not so worried about leaking stuff, I can just call the Product model from within.

And now since this is inside the Product module, I'm not so worried about leaking stuff, I can just call the Product model from within. And this is what I'm going to do, I'm going to say Product::find($productId), let me add the query. This is just something I like this is a personal preference. I don't know if it is going to find a product. So I'm going to add this operator. So we only execute this method. If it is a null. And we're going to say decrement stock by the given quantity.

If it is a no. And we're going to say decrement stock by the given quantity. Let's rename this to amount that makes more sense. Now within our CheckoutController, we can inject a class and we're going to use levels container to inject it for us called ProductStockManager, ProductStockManager. And right here where we are doing this, where we are decreasing the stock of the item, we're instead going to call the ProductStockManager. We have the productId, whoops, typo. And we have the quantity, which is quantity, right, let's rerun this. Still failing.

And we have the quantity, which is quantity, right, let's rerun this. Still failing. Let's see what is it? A defined property of Product to tell price and cents. Yes, we've changed this on the DTO is just called price and cents without the underscores. And now we're back to green. All right. So let's see what we did. First, we replaced it with calculation logic we had by creating a CartItem collection. We're grabbing data from the checkout.

First, we replaced it with calculation logic we had by creating a cartItem collection. We're grabbing data from the checkout. We could also encapsulate that within the request object. Some people find it dirty. I personally like it, but let's keep it as this is right now. We're instantiating paybody as usual. We're creating the order as usual as well. And then we have the productStockManager acting to decrement stock from a product. Now you might be thinking this isn't necessary, and sometimes it is. But if you're going with a modular approach, you ought to think about those things.

Now you might be thinking this isn't necessary, and sometimes it is. But if you're going with a modular approach, you ought to think about those things. This is a very simplified example of an e-commerce app, but most e-commerce applications have much more sophisticated stock management. And stock management would probably be a module itself because it gets fairly complex. So it makes sense to defer this call to a different class whose sole responsibility is to manage stock. And now we also, basically what we're doing is we're getting everything that's not related to the module we're in and moving it towards another module. That makes this class, the ProductStockManager class, very, very easy to test.

to the module we're in and moving it towards another module. That makes this class, the ProductStockManager class, very, very easy to test. Even if it's complex, it increases. Even if the way we handle product stock management becomes much more complex. And the same applies to the CartItem. Not that we have this encapsulated in another class, it's also much easier to test, including this method, for example. We're starting to take notice of our boundaries and extract what shouldn't be on the boundary we are at, which is the Order module. You can also see that we are no longer licking the Product model within this class.

we are at, which is the order module. You can also see that we are no longer licking the Product model within this class. We don't have access to it. We have access to the DTO, which holds all the information we need, but we do not have access to the Product model itself. If we want to do some sort of write operation against it, we defer that to another object. All right, that's enough for this lesson. We're going to continue refactoring this and introducing additional modules as we go. See you guys on the next lesson. Bye bye.

See you guys on the next lesson. Bye bye.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟