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

Casting Value Objects0:00

Hey, welcome back to wrap up our value objects journey. Let's see how we can use value objects with Eloquent. That is how can we instruct Eloquent that a property should actually be casted into a richer object? This is very, very straightforward with Eloquent. So this will be a very quick lesson. So let's also use this as an opportunity to cl up some bits over code. With no further ado, let's jump into the code.

Writing the Cast Test0:21

to cl up some bits over code. With no further ado, let's jump into the code. Here we are, this is the invoiceTest.php file and I just wrote a very simple test. We're just saying. The total paid is casted into a Money object and all this test does is ensuring that total amount is an instance of Money. This is a pretty simple task file. The only other thing going on is that on the setup,

Creating Custom Cast0:47

This is a pretty simple task file. The only other thing going on is that on the setup, we are issuing an invoice to be used throughout the task cases. If we try to run this task, surely it fails as it should. So let's fix that to construct Eloquent, to cast a property into a richer object, we need a class. So we're going to go to CLI and say php artisan make:cast and we're going to call this MoneyCast. As you can see, a new directory was created under app

and we're going to call this MoneyCast. As you can see, a new directory was created under app and here we go. We have a MoneyCast class. As you can see, this class implements the CastAttributes interface and it only has two methods get and set. get hydrates that raw value into the object we want and set unserializes that richer object into something that the database understands.

and sat the sales Eloquent how to unserialize that richer object into something that database understands. If we go into the Invoice model, we can see that totalAmount is currently being stored as a decimal. So we know that we can cast it into a Money object by saying Money::fromMajor and we can pass the value. How do we know what value is? Well that's because within our Invoice model we're going to say that we want to cast it into a MoneyCast class. So we just passed the fully qualified

to say that we want to cast it into a MoneyCast class. So we just passed the fully qualified namespace for that class. Eloquent is smart enough to pass the correct parameters. So for model we're gonna get an instance of Invoice for key, we're gonna get totalAmount. That is a property we're trying to access for value. We're gonna get its raw value and for attributes we get all of the attributes on the model if we wish.

and for attributes we get all of the attributes on the model if we wish to access any of them. So we can get rid of this. And setting a value is slightly more complex because we want this to be backwards compatible. So let's talk about this one later. Let's go back to our test and test this out. Cool. And it's passive. We also gotta make sure that it has the right value.

Implementing Set Serialization2:38

And it's passive. We also gotta make sure that it has the right value. So let's say something like assertEquals, and if we scroll up a little bit, we can see that this invoice is worth a hundred, a hundred, $200. So we want this to be equal to to a hundred dollars. Let's do something like this and let's rent this and it's passing. And now we want to task the set method that is that we can actually save that invoice instance.

And now we want to task the set method that is that we can actually save that invoice instance. So let's go ahead into the following. We're gonna call this invoice and we're gonna say invoiceFactory. We want to create an invoice and we're gonna say that the total amount is going to be money from major to 150. And then let's just make sure that this is correct. Let's try running this. And you can see it fails.

And then let's just make sure that this is correct. Let's try running this. And you can see it fails. It says object of class Money cannot be converted into a string. And that's because we haven't told Eloquent how to turn that Money object into something the database can understand. So let's go into money cast and here's a trick. This is an existing application that is we already have things running on this application.

This is an existing application that is we already have things running on this application. You already have places where we're passing a primitive instead of an object when setting the total amount. So let's handle that. Here's what we're gonna do. We're gonna say if value, and you can see the value is mixed. So it could be anything as an instance of money. We're gonna return money, I'm sorry, value major and let's rerun our test and now we're back to green.

We're gonna return money, I'm sorry, value major and let's rerun our test and now we're back to green. So if we got an instance of money, if we got a rich object, we're gonna digitalize that into something the database understands and if we got something different, we're just gonna store that because that's what we were doing previously call, that's all others know on how to serialize those which are objects using Eloquent. That's as simple as that.

Fixing IDE Type Hints4:28

to serialize those which are objects using Eloquent. That's as simple as that. You just create a class, you make it implement cast attributes and you just tell Eloquent how it should get the property and how it should set the property. That's it. Before we move on, something important, you might notice that if I hover over totalAmount, it is saying needs a float, but we know for a fact that this is actually a money object.

it is saying needs a float, but we know for a fact that this is actually a money object and that's because your ID probably does not know how to retrieve that. So if you're using phpstorm with laravel-idea, which I am, you can use its helper code generator and I'm gonna run it now. So generate helper code. I'm gonna run this, let's go a second. And now if I hover over it is telling me that it's mixed

I'm gonna run this, let's go a second. And now if I hover over it is telling me that it's mixed and that's because I forgot to type Money this guy. So we can say this actually returns a Money object. Let's try again. And there we go. You can see that my ID is telling me this is a Money object. If you're not running Laravel ID or if you're not running phpstorm, you can use the package laravel-ide-helper. It will generate an ID helper file.

Updating App for Money5:33

you can use the package layer avail ID helper. It will generate an ID helper file with those richer types for you. Okay, cool. Let's move on. If we run all of our tests on this file, you can still, we have a test failing and it's sky right here. Task. It pays for an invoice and it's failing on this line. So we're calling this paymentGateway process method and this method expects to float, but we know that totalAmount is a money instance.

and this method expects to float, but we know that totalAmount is a Money instance. So let's fix this with a very quick fix. Ideally, I think this guy should exactly be a Money object, but for now we just want to get back to green. Let's just call major in this guy and rerun our test. It's still failing but in a different place. 1 41. Okay, so we're getting the total of payments, the amount of payments and making sure it's greater than or equal to the totalAmount, but totalAmount is an object.

of payments and making sure it's greater than or equal to the total amount, but totalAmount is an object. So let's do something like major. Let's rerun our tasks and 200 does not match. Expect the type object. Well same thing. This is an object, so this is never going to work. Let's just say major here and move on with our day. Okay, we're back to review on this file. Now let me run all of our tests. Hmm, we have a couple of failures

Now let me run all of our tests. Hmm, we have a couple of failures and we have some failures on the Invoice. InterestSimulationController, the file we were working on in the last video, let's jump to that file and let's jump to its test. Let's run the first test. Same thing. Let's see where it's failing totalInterest. Okay, so I think we can make some improvements here. The first thing is let's say

Okay, so I think we can make some improvements here. The first thing is let's say that we want this returning money object and now we can do something like this. We know $totalAmount is already a money object and on the last lesson we added a method to apply interest, but that applies interest on top of the existing value. Let's add a method to just calculate that interest. So let's say calculateInterest and we're going to pass an $interestRate.

So let's say calculateInterest and we're going to pass an interestRate. So from decimal and we're gonna say totalInterestPercentage, which is not really a percentage, but it's decimal representation. So we have to fix that later. And now we gotta go to money and add that method. Let's say calculateInterest. This is going to take an interestRate.

Let's say calculateInterest. This is going to take an interestRate and this is going to return an instance of itself. Let's just copy what we have on the other method. There we go. I think we could also refactor this a little bit. So let's go to our MoneyTask class and let's run a test just to make sure we don't break anything. Here's what I think we could do on this. applyInterest.

to make sure we don't break anything. Here's what I think we could do on this. Apply interest. I would like a more expressive API. I like to do something like this, this->add, and then we can just say calculateInterest and pass the interestRate. So we just want to add a value on top of the existing one. Let's add this method. We expect a money and we're going to return an instance of itself.

We expect a Money and we're going to return an instance of itself and you can say something like, we want a new instance of the same class with the existing amount of cents plus the amounting cents of the other instance. Let's run our tasks on money tasks just to make sure we haven't broken anything and we're green. Great. Now let's go back to invoice interest simulation. I'm actually going to close the other tabs. Let's go back to this jazz and rerun it.

Allowing Zero Money8:30

I'm actually going to close the other tabs. Let's go back to this jazz and rerun it. And we're still failing amount needs to be greater than zero. Okay, that's interesting. We had previously coded this value object to only accept values greater than zero, but it seems we might need to also accept zero. This is actually also done in real life sometimes for example, when you have shopping cards.

This is actually also done in real life sometimes for example, when you have shopping carts and people can add coupons and not pay anything, you can represent that with an object like a Money object with a zero amount. And if you have a context in your application, a different context where you always expect to receive something, some monetary value, then you can have a separate value object for that. That only accepts values greater than zero.

then you can have a separate value object for that. That only accepts values greater than zero. So let's suggest this. We'll have to go back to our MoneyTest because we have a task. Testing that Task requires the amount to be greater than zero. So let's get rid of this and you can say test, it allows zero values. Why have a Task for this to ensure we don't have regressions.

Why have a task for this to ensure we don't have regressions. So this test is pretty simple. We're gonna say expect not to perform assertions since we don't have any assertions going on here. Or we could just do something like this where we could say assertEquals zero and we're just gonna call major in this guy, let's run this. It's failing now let's fix our money implementation.

and we're just gonna call major in this guy, let's run this. It's failing now let's fix our money implementation or equal to zero and we're back to green. Let's run all of our tasks here and they're all green. Great. So let's go back to our InvoiceInterestSimulationController tasks. Let's run this and we're failing, failed starting the two matches expected 0.02. Okay, at some point we're returning the percentage instead of the decimal representation.

Okay, at some point we're returning the percentage instead of the decimal representation. Let's look at our serialization strategy and it's the sky. So let's do decimal representation instead. This was already broken from the last lesson. Let's rerun a test. Still failing. Let's see what's failing. It's an assertion. Okay, so we are expecting $totalInterest to be zero, but we're getting something weird and that's because we've changed what $totalInterest is. $totalInterest was previously aloed

because we've changed what totalInterest is. TotalInterest was previously allowed and it is now a Money object. So let's just call major on this guy. Let's rerun your tests and we're back to green. Let's run the other tests. They're also passing. And let's see, what do we have failing? Okay, we have one test failing and it's an assertion. So object does not match expected type. Double this float right here. Let's suggest that.

So object does not match expected type. Double this float right here. Let's suggest that. Let's just say we expect this to be money instance, let's create for major, let's run all of our tests and we're back to green. Now, before we wrap this up, let's go back to our cast object and let's try to narrow down this type a little bit. Right now we're accepting two different things. We have a specific code path for a money object,

Enforcing Money-Only Inputs11:21

Right now we're accepting two different things. We have a specific code path for a Money object, but we still accept primitives. And ideally, I like to accept a single thing. So let's see what happens if I get rid of this, if I just return value major and always expect value to be a Money instance. In fact, we can add an assert call here. We can say that we expect value to be an instance of Money. Let's run a task and see what happens.

We can say that we expect value to be an instance of money. Let's run a task and see what happens. Okay, we have a couple of failures. The first one is on invoice line 76. So let's see what we have there. Okay, this is where we're instantiating an Invoice and totalAmount here is being passed as a float, but it should be money instance. So let's fix that. Let's rerun our tests. Okay, now we only have nine failing.

So let's fix that. Let's rerun our tests. Okay, now we only have nine failing. SendInvoiceController tasks. 35, let's see what we have that. And this is a factory, so we probably have to adjust this factory. There we go. Let's say money from major. Let's run our tests to failing. We're getting close y'all. InvoiceInterestSimulationControllerTest. Let's run this here. And we have

Invoice interest simulation controller test. Let's run this here. And we have a set trace, and this is because we're creating an invoice. We gotta make sure this is a money object and we also have another failure. Same thing. Let's make sure this is a money object. Now let's run all of our tests. And there we go. We are back to green. Let's go back to our money cast. And now let's talk a little bit on whether you should do this.

Choosing Compatibility Strategy13:02

And now let's talk a little bit on whether you should do this. If you have an existing application and you are already passing parameters around and now you have richer types, then you should probably maintain both code paths. Unless you have really good test coverage and you're willing to fix all of the occurrences, just maintain two code paths, make sure you have tasks for them and you're good to go.

just maintain two code paths, make sure you have tasks for them and you're good to go. And as time goes, you can reduce this to a single code path. If this is new code, if this is a new property, a new model, and you already wanted to start with you reach your object, then you can maintain a single code path here. I recommend using cert and the reason I recommend using cert is because certs a little bit different than exceptions by default will not fail in production,

because certs a little bit different than exceptions by default will not fail in production, but it will fail locally unless you have changed your PHP settings, that's how it's gonna behave. So you can have an assert here making sure this is an instance of Money, but you can still maintain a different code path like this. If it's not an instance of Money, you just return the value. And this will work just finding production,

If it's not an instance of money, you just return the value. And this will work just finding production, but it will fail locally. So if you run your tasks or if you're just testing things manually, you will stumble upon an error and that's an opportunity to fix it, but it will not affect your users in production. Once you're sure you don't have any other code paths or if this new code, you can just get rid of this.

Once you're sure you don't have any other code paths or if this new code, you can just get rid of this and just assume this is always the object you expect. And of course, we can also narrow down this type hand rate here, major returns of float. So we can say this returns a float, let's rerun our tests and we're good to go. As you saw, casting properties into richer objects is super, super easy with Eloquent, so much so that we spent most of the lesson doing other things rather than seeing.

super easy with Eloquent, so much so that we spent most of the lesson doing other things rather than seeing how we can cast those properties. As usual, I hope you enjoyed this lesson and I'll see you in the next one. Bye.

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