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

Defining Domain Invariants0:00

One thing that we've spoken quite a bit about is making a Lego state representable. And a good way to do that is by enforcing domain new variants. If you don't know what a domain new variant is, it is basically a rule that must always be true in a given context. For example, within an e-commerce context, you must have a rule that a canceled order can never be shipped.

you must have a rule that a canceled order can never be shipped. That is always true. That rule is always valid, therefore, it is a domain variant. Or if you have a calendar event system, you can ensure that the start time is always before the end time, that it must also always be true. And now you might be thinking what is the difference between domain variance and request validation?

Validation vs Domain Rules0:42

And now you might be thinking what is the difference between domain variance and request validation? And to give things simple, I'm just gonna say that validation is an application concern. It happens very early on the request. So you get a request, it reaches a controller, you run validation, and then you run your domain logic. That means that you can skip validation, maybe you can forget to validate something with domain variance, we're adding the role to the core.

maybe you can forget to validate something with domain variance, we're adding the role to the core of your application to your business roles. Therefore, we make a Lego state impossible. For example, if you have a method to add a calendar event within the method, you're gonna validate that the startTime is before the endTime. So it'll be impossible to add an invalid event to a calendar.

Adding Currency to Money1:19

So it'll be impossible to add an invalid event to a calendar. Let's jump into the code and see a couple of examples. Okay, so an improvement we could make to our Money class is to also accept a currency. So let's do something real quick. Let's say that we're gonna have a public currency, currency, currency, and we're gonna create this class. There we go. This is actually going to be an enum and we can do something like this.

There we go. This is actually going to be an enum and we can do something like this. We're gonna support USD and we're gonna support the euro. And for now, we're just gonna say that by default we're gonna use UST. Let's run our tasks to make sure that everything's passing. And now if we go down to add, what happens if I try to add an Euro money instance on top of an UST money instance, that shouldn't be possible and that's in a variant we can enforce.

of an UST Money instance, that shouldn't be possible and that's in a variant we can enforce. So let's go to our MoneyTask class and let's set a task here. Let's say task. It cannot add to monies of different currencies or with different currencies, something like this. And now let's write a simple task. Money is going to be money for major,

And now let's write a simple task. Money is going to be money for major, and then we're simply going to add money on top of it. And this is going to be for major. Let's do 50 and let's pass a currency. So let's add support for that as well. This is going to be Euro and this is going to be USD. Let's go back to our class and let's just implement this so we can say that we're also going to take a currency

and let's just implement this so we can say that we're also going to take a currency and by default that is going to be USD. Let's pass that here. There's a typo right here. Lemme just fix that. Cool. Let's run a task to make sure everything's green. Okay, let's go back to the task we were working on. And now let's tell phpunit that we would expect this to fail. So let's say expectException,

that we would expect this to fail. So let's say expectException, and we can see something like InvalidArgumentException. Let's go with that one for now and let's try this. And you can see the task is failing because nothing is failing. The actual code is not failing. So here's what you can do. Let's go to add. And we're gonna do something like this. If the money already received, if it's currency, it's not the same actual let's in convert that.

If the money already received, if it's currency, it's not the same actual let's in convert that. Let's do, if the current currency is not the same currency as they're receiving money, we're gonna throw a new Exception. Let's do InvalidArgumentException. And we can say, let's do something fancy. So let's say something like this, let's do sprint and we're gonna say, cannot add money with currency to currency like this.

And we're gonna say, cannot add money with currency to currency like this. And now let's pass some arguments here. So in practice, what this is going to say is not add money with currency, euro to currency, USD. Let's rerun our test energy. And now if we remove this just so you can see what exceptions being thrown, you can see that we say cannot add money with currency, euro to currency, USC.

Prevent Paying Paid Invoices4:14

that we say cannot add money with currency, euro to currency, USC. This is a domain theory. It is always held true. It must always be true. Let's see where else we can do this. So let's re-add this just so we get the stats back to green. Let's close this. And now let's look at invoice. If we scroll down a little bit, we have a pay method. And what could happen here is right now we could pay an already paid invoice or a canceled invoice.

right now we could pay an already paid invoice or a canceled invoice. So let's also add a business rule right here. We already have a task for the pay invoice action. So let's jump there and let's add a task. Let's say task, it fails with a paid invoice. And then we can do something pretty similar to what we did on the money test. So here's what we're gonna do.

what we did on the money test. So here's what we're gonna do. We are going to pay for the existing invoice and we're gonna use a cash payment gateway. So at this point, the invoice is paid and now let's fake our mail. Here's what I plan to do. I wanna try charging an invoice. So try paying for the invoice, then I want to make sure it failed and I want to make sure nothing was sent. So here's how we can do this.

it failed and I want to make sure nothing was sent. So here's how we can do this. We could simply use expectException and expect this to fail, but I'd like to add some assertions on top of it. So here's what I'm gonna do. I'm gonna add a try-catch block, and I'm gonna say I want to pay an invoice and now I'm going to pay an invoice that has already been paid.

and now I'm going to pay an invoice that has already been paid. So the exact same thing we did up there, and now I'm going to catch UnspecifiedException. So for example, let's do InvalidArgumentException. And now you can do some assertions on it. So we could do something like this. We can say that the message will be something like "the invoice is already paid." We could make sure the invoice is actually paid.

the invoice is already paid. We could make sure the invoice is actually paid with this invoice is paid and we could assert nothing was sent and then we could simply return. And then down here I could do something like this. fail expected exception was not thrown. So here's what we're doing. We are writing some code, we're writing this code, and then we have a catch block that is if it throws an exception,

we're writing this code, and then we have a catch block that is if it throws an Exception, we're gonna execute this block. We're doing some assertions and then we are returning early, which means that this method will stop executing right here. If the Exception is not thrown or if a different Exception is thrown, this block will never be reached. And then we'll get to this call right here,

this block will never be reached. And then we'll get to this call right here, which tells speech punitive fail. Let's give it a try. Let's run this. And there you go. Expected exception was not thrown and that's because we're not throwing anything. So let's go to invoice and let's add a small block here. We're simply gonna say if this is paid, we're gonna throw an exception and InvalidArgumentException is not the right exception here.

we're gonna throw an exception and InvalidArgumentException is not the right exception here. We should probably do the main exception. The invoice is already paid. And now let's just change this to expect DomainException. Actually, let's leave it as it was. And you can see it is still going to fail. Now if I change this to DomainException, it is passing. So that's pretty cool. Here's, you can see the difference

Now if I change this to DomainException, it is passing. So that's pretty cool. Here's, you can see the difference between a validation rule and a domain variant. This is always going to be enforced because this is at the core of our domain model. It's where the state change is happening. The state change is never gonna happen unless that rule is fulfilled. So there's no way for us to pay for NRA paid invoice. Now there are some improvements that we could make here,

Creating Custom Domain Exceptions7:51

So there's no way for us to pay for NRA paid invoice. Now there are some improvements that we could make here, at least I consider that an improvement. And one of the things we could do here is throw a custom exception that really expresses what happened. For example, let's go to exceptions and I'm gonna create a new class, let's call it CouldNotPayForInvoice. We're gonna have this extend a RuntimeException. And then let's go here

We're gonna have this extend a RentTimeException. And then let's go here and say that we wanna throw a CouldNotPayForInvoice. I find this much more expressive than DomainException. So let's go to our test and let's update this to take a CouldNotPayForInvoice, let's rerun it or green. And something that I really enjoyed doing, something that I picked up from my friend Sean Nicole, is to use static constructions on exceptions.

that I picked up from my friend Sean Nicole, is to use static constructions on exceptions. Something like this, let's say you could not pay invoice and then we could simply do something like this because the invoice is already paid. I find this so, so expressive. I really like this way of doing things. So let's go ahead and go to our exception, let's add a static method. This going to return south,

let's add a static method. This going to return south, and we can simply that say, could not pay for the invoice because it has already been paid. Let's copy this. Let's update our task, expect that message, and let's rerun it. Oops, forgot the new keyword here. Let's rerun this. And we're back to screen Now to add the final touches. We could go here and we could add a doc block. We could say that this throws a could not pay

We could go here and we could add a docblock. We could say that this throws a CouldNotPayForInvoice and we can get rid of this. And then since DB transaction could also throw an exception, let's make our $id happy and also add throwable here. Let's also go back to our Money class and add a docblock to this guy. Let's say that it could throw an InvalidArgumentException if we wanted. We could also have a custom exception for this one.

exception if we wanted. We could also have a custom Exception for this one. But I think in this case, InvalidArgumentException actually fits the case very well. It's exactly what's happening. This is an invalid argument for this operation because they do not share the same currency. So I think this is fine. And now finally, to give a rather unorthodox example, but something that could be true in some applications,

Client Deletion Business Rule10:06

And now finally, to give a rather unorthodox example, but something that could be true in some applications, imagine that one of your business rules is you cannot delete a Client that has any Invoices. This is the rule, pretty arbitrary, but it is the rule your company decided on. We could go to Client to the Client model. And this model has a delete method, right, that we're gonna use throughout the app,

And this model has a delete method, right, that we're gonna use throughout the app, and that's the source of truth. That's what's going to be called to delete the model. And we can apply rows to that as well. We can apply inference into that operation. So in this case, delete is an Eloquent method, which means we would have to overwrite the method. So we could say something like this, let's say delete, and we're gonna call the parent, delete.

So we could say something like this, let's say delete, and we're gonna call the parent, delete. This is going to return bool, or no, let's double check yes bool or no. And here we could check, we could say something like this if invoices exist, if there are invoices, we want to throw an exception, something like ClientCouldNotBeDeletedException. Let's actually add this exception right here ClientCouldNotBeDeletedException.

Let's actually add this exception right here could not be deleted. And we want this to extend RuntimeException or DomainException if you prefer. And let's also do a static construction. Let's say something like because there are active or unpaid invoices or because there are invoices, if we want to make sure, for example, for accountability or login purposes, you should not be able to delete a client.

for example, for accountability or login purposes, you should not be able to delete a Client because there are invoices. And this is going to return an instance of itself. And we can simply say, clients with invoices cannot be deleted. And now in our Client model, we could simply say, Client cannot be deleted because there are invoices. Super, super expressive. And then adding the test for this is super simple.

Super, super expressive. And then adding the test for this is super simple. So as you've seen the difference between validation rules and domain rules is that domain rules live in the core of your business rules. They must always be true, whereas validation rules depend on the context. For example, if you have an e-commerce application, you might allow more

For example, if you have an e-commerce application, you might allow more and more customers to only order up to 20 items, but you might allow enterprise customers to order up to 1000 or to 1000 items. Those are validation rules in most contexts. The domain variance are things or roles that must always be true at any given point in time. For example, that a shipped order cannot be shipped,

any given point in time. For example, that a shipped order cannot be shipped, that's a pretty core role. It must always be true. And by enforcing those variants within our domain, we can ensure legal state is never reached. Now, you might be thinking this is a project that has richer models. So we have lots of methods that do state changes on the models themselves.

So we have lots of methods that do state changes on the models themselves. But what if I have a project that leverages a lot of actions and thin models? Where should I place those in variables? And the answer is the same thing. You still have your business rules, they still exist, they're just expressed in a different place. So you may be expressed in an action. For example, you have a very thin model,

So you may be expressed in an action. For example, you have a very thin model, it just communicates with the database and you do all of your business logic, your domain logic within actions. This is where you would place those domain variants. The important thing is that they're coupled with those business rules so that you can never reach your legal state. You make your legal state un representable, as we've seen,

that you can never reach your legal state. You make your legal state un representable, as we've seen, it becomes impossible to add, for example, 500 years on top of $500, or it becomes impossible to pay for an invoice that's already paid. Even if you managed to skip validation, even somehow validation doesn't apply. The state change would never happen because it would fail earlier. This is a rather simple but very, very powerful concept.

because it would fail earlier. This is a rather simple but very, very powerful concept. As usual, I hope you enjoyed this lesson and I'll see you on the next one. Bye.

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