Why Use Interfaces0:00
Hey there, welcome to a new lesson. One thing we haven't spoken about are interfaces. We've spoken about data objects, we've spoken about sharing contracts between modules, but not about interfaces specifically. And we have a really good use case for an interface on our chatbot flow. Remember that the PayBuddy implementation that we're using is an SDK, really. Think about Stripe or PayPal or any other payment provider. They usually provide you with an SDK that you can use on your project,
Think about Stripe or PayPal or any other payment provider. They usually provide you with an SDK that you can use on your project, a package that you can use, and you usually want to have an abstraction layer between your application and the actual SDK. On this lesson, we're going to create that abstraction layer and see where we can use interfaces to make our lives easier. So let's jump into the code. All right, so this is our controller. Let's start by running our tasks and making sure that everything works.
All right, so this is our controller. Let's start by running our tasks and making sure that everything works. Cool, we have the green light to make changes. All right, so this PayBuddy SDK, it really isn't something we own. Of course, this is an example, but imagine if we had Stripe or PayPal. We would be using their SDK, their package, and again, it's not something we own. In those situations, it makes sense to add a bridge between our application and the SDK to add an abstraction. And this is also where using an interface makes a lot of sense.
between our application and the SDK to add an abstraction. And this is also where using an interface makes a lot of sense. First, because we might have multiple implementations, such as Stripe or PayBuddy or PayPal. But even if you only have one gateway, which is usually the case, you could also have an in-memory implementation to use in tasks. That's pretty much what Laravel does when you fake a facade, for example. When you fake the mail implementation, you're using an in-memory implementation that doesn't actually send an email. And we also use an interface to have a common denominator.
you're using an in-memory implementation that doesn't actually send an email. And we also use an interface to have a common denominator for both implementations. That is, a contract that they both follow and that we can type in. So behind the scenes, this is what's happening. We're swapping the implementation with a fake implementation, and they both follow the same contract, the same interface. The same thing happens with cache drivers. You can use the file cache driver, you can use Redis, you can use the array cache driver,
Define PaymentGateway Contract2:10
You can use the file cache driver, you can use Redis, you can use the array cache driver, and they all follow a contract, they all follow an interface. So let's think about this for a while. First, let me rename this to PayBuddySDK so we know what we're talking about, because, again, this isn't a package. This is something we added to the project. Okay, there we go. Let's start by creating a contract itself, the interface. So we can go to phpStorm and create a class.
Let's start by creating a contract itself, the interface. So we can go to phpStorm create class. We want this to be called an interface, and we're going to say paymentProvider, or rather paymentGateway. Here's an example implementation. We can have a charge method, and since we own this now, we can expect a PaymentDetails object. There we go. Let's not fill it with anything for now. And we don't have to return an array or a string. We can actually return an object we own.
Implement PayBuddy and In-Memory3:01
And we don't have to return an array or a string. We can actually return an object we own. So, for example, we can return a successful payment. Okay, now we have a contract to follow. Now, let's create two classes. First, I'm going to create the PayBuddy gateway, and then I'm also going to create a class called InMemoryGateway. Gateway might not be the best word here, but let's go with that for now. And they're both going to implement the PaymentGateway contract. Let's add the method stubs. There we go.
And they're both going to implement the PaymentGateway contract. Let's add the method stubs. There we go. And we also have the PayBuddy gateway. Let's do the same thing. Let's implement PaymentGateway and add stubs. Okay, so we now have an abstraction layer between our application and the SDK. For the PayBuddy gateway, the first thing that I know we're going to have to do is to inject the PayBuddy SDK right here. So let's say that we expect to get an instance of PayBuddy. And on the charge method, we're going to first proxy this to PayBuddy.
So let's say that we expect to get an instance of PayBuddy. And on the charge method, we're going to first proxy this to PayBuddy. We're going to fetch the token from the details. What is the second argument? The amount in cents, and those properties do not exist yet. And then the statement description. There we go. Let's add this property. Actually, I want to use property promotion, and I don't think PHPStorm does that. So let's do this manually.
Actually, I want to use property promotion, and I don't think phpStorm does that. So let's do this manually. We want a token. We want the amount in cents. And we want the statementDescription. We also want this class to be read-only. We're not planning to mutate it. There we go. We can obviously get rid of this. All right, so we have a charge.
We can obviously get rid of this. All right, so we have a charge. If we look into the SDK, we can see what it returns. Usually have documentation telling you what it returns. If you're lucky enough, the SDK is not going to return an array. It's going to return an object. But basically, what's interesting to us is the id and the amount in cents, which we already have because we're passing this. So on this successful payment object, let's return something like this. We want to return a new successful Payment,
So on this successful Payment object, let's return something like this. We want to return a new successful Payment, and we want to pass the chargeId and also the amount in cents. Let me actually confirm if this is what we're calling it. Yes, amount in cents. So on the successful Payment object, let's also create a constructor. We expect the id. We expect the amount in cents. And something we're missing is we also expect the provider. So let's go ahead and create an enum called PaymentProvider.
And something we're missing is we also expect the provider. So let's go ahead and create an enum called PaymentProvider. We want this to be an enum. This could be back to enum. So we have payBuddy, and we also have inMemory, which we're going to call in_memory. Okay, so we have an enum. Let's expect that as well, so PaymentProvider. And this is just to identify the payment provider. So we have to pass that.
And this is just to identify the payment provider. So we have to pass that. We can actually go to our contract and also create a method called IT, which is the identifier of the gateway, which returns a payment provider. So we have to now implement this. Let's implement this on the inMemoryGateway, which is just paymentProvider in memory, and now for the payBuddyGateway as well. paymentProvider, payBuddy. There we go.
Payment provider, pay buddy. There we go. So we can actually use this method. Let's format this and make it a little easier on the eyes and pass the identifier, for example. So now we have a successful payment that specifies everything we need. The payment ID, the amount in cents, and also the payment provider. Perfect. They both follow the interface. We actually need to show this method on the in memory gateway.
They both follow the interface. We actually need to show this method on the inMemoryGateway. But for now, let's just return a successful payment. We need to pass a chargeId. So for now, let's generate a random euId using the Str helper. We need the amount in cents, which we have in the details, and we have the identifier as well. There we go. Okay, so we now have two implementations following the PaymentGateway contract. For now, we're not going to use the inMemory implementation.
Bind Interface in Container7:01
Okay, so we now have two implementations following the PaymentGateway contract. For now, we're not going to use the in memory implementation. We're going to use that in tasks in the future. So let's focus on the PayBuddy implementation. Since we now have that interface, we don't actually have to specify the gateway we want to use. We can inject it, right? So let's do something like we want to inject a paymentGateway, and now we can pass this into our payment. There we go.
and now we can pass this into our payment. There we go. Obviously, we're not following the correct type. So if you try to run the task, first, let's see what we got. So targetPaymentGateway is not instantiatable. That's obvious. Since we're trying to instantiate an interface, Laravel's container is not going to understand how to do that. To fix that, we have to register that in a provider. So let's go into our ServiceProvider,
To fix that, we have to register that in a provider. So let's go into our service provider, and in the boot method, we can register that. We can call app, and we can tell Laravel, hey, whenever someone requests payment gateway, we can pass a closure and say, hey, give them an instance of PayBuddyGateway, and we can pass the PayBuddySDK right here. Sorry, it's not register. My bad. It's bind. Let's rerun this.
Sorry, it's not register. My bad. It's bind. Let's rerun this. Okay, failing. What do we have now? PendingPayment construct expects PayBuddySDK, and we're passing an instance of paymentGateway. So let's fix our other classes. PendingPayment does not expect a PayBuddySDK. It expects a paymentGateway. Inside purchaseItems, we're also calling our paymentGateway. So we have createPaymentForOrder,
Inside purchase items, we're also calling our payment gateway. So we have createPayment for order, which expects an instance of PayBuddySDK, but we're now passing an instance of paymentGateway. So let's update that as well. We want an instance of paymentGateway. It doesn't matter which. Let's call this paymentGateway, and then we can replace this with paymentGateway. The signature is different as well.
and then we can replace this with paymentGateway. The signature is different as well. We now pass an object. So let's say paymentGateway->charge, and now we can pass paymentDetails object. Let's import this. So here we can pass the token. We can pass the amount in cents, and we can pass the statementDescription, which is modularization.
and we can pass the statement description, which is modularization. All right, let's run our tasks. Still failing. Let's see what's going on. Let's go into our tasks, and let's make sure that Laravel's exception handler is not doing some magic here. Let's call the withoutExceptionHandling method and rerun the tasks. Okay, now we get a proper error.
and rerun the tasks. Okay, now we get a proper error. So we cannot use an object of type SuccessfulPayment as an array, and that's a problem. We're trying to call the it index on an object. So let's just replace this since now the return type has changed, and we can just call it. Obviously, we can also update the paymentGateway
and we can just call IT. Obviously, we can also update the payment gateway to return the payment provider. Let's see what happens. Okay, yeah, so it's now pay_buddy. Let's update that in our tasks, and to be honest, we don't really want a string here. We want an enum, so let's say that we expect this to be payment_provider pay_buddy.
so let's say that we expect this to be payment provider PayBuddy. Let's rerun the tasks. Yeah, PayBuddy does not match expected type object, so we need to tell Eloquent to cast that column into an enum. Let's go here and use the casts property and say that we want the paymentGateway to be casted into a PaymentProvider. Let's rerun the tasks,
to be casted into a payment provider. Let's rerun the tasks, and now we're back to green, and we removed the hard dependency we had on the paybuddy SDK. So we now depend on a payment gateway, which might be either a paybuddy gateway or any memory gateway. Now, another thing we can do since we own this implementation
Wrap SDK Exceptions10:39
Now, another thing we can do since we own this implementation is to throw proper exceptions. The PayBuddy SDK is not really it. It throws a generic exception. We can start throwing our very own exceptions. For example, we know that this could throw a RuntimeException. Let's catch this. We can catch the RuntimeException,
Let's catch this. We can catch the RuntimeException, and in some situations, you might have things like the SDK always returns something, and you have to do, for example, chargeSuccessful or success, and then do your logic. In this case, this Turbo SDK throws a RuntimeException.
In this case, this Turbo SDK throws a runtime exception. We can catch that, and we can throw a PaymentFailedException, which right now is on the Order module. I think it makes more sense to have that within Payment module, so let's move that. Let's go here. Since I'm using phpStorm,
Let's go here. Since I'm using phpStorm, I'm going to create a directory called exceptions, and then I can simply drag and drop this from within the order module into the exceptions. On the payment module, and it already updates the namespace for me. We can throw that. Now, we don't know if this was due to an invalid token,
We can throw that. Now, we don't know if this was due to an invalid token, so if we're not sure, sometimes you just have to pass the exception message. For example, PaymentFailedException, and we can pass the exception message. If you have a way to know what failed exactly, it's better to throw more specific exceptions. In this case, we cannot.
it's better to throw more specific exceptions. In this case, we cannot. Now, on this piece, we don't have to catch the RuntimeException because we're throwing those from our implementations, and you can just remove this, and this exception will flow upwards until we catch it. How do I format this?
until we catch it. How do I format this? All right, let's rerun our tasks. Okay, they're surpassing. We're throwing this new PaymentFailedException, and we're catching it within the controller to give the user a proper response. We can also add a docblock telling everyone that this throws an exception like this.
Recap and Testing Strategy12:35
telling everyone that this throws an exception like this. All right, so we have now moved away from the PayBuddy SDK implementation into our very own abstraction, and now we have two implementations, really. We have the PayBuddy implementation and the Enamor implementation. They both follow an interface. When we talk about interfaces,
They both follow an interface. When we talk about interfaces, a really common example is you could have a PaymentInterface with multiple providers. So you could have Stripe, you could have PayBuddy, you could have PayPal. I don't think that's very realistic. Most businesses only have
I don't think that's very realistic. Most businesses only have one single payment provider, but you usually want to have a second provider, a fake provider, to use in tasks, and this is what we have right here. We have our PayBuddy implementation and also an Enamor implementation.
We have our PayBuddy implementation and also an Enamor implementation. We are not doing anything with it right now, but we are going to in the future. For example, if you were to task the class that's responsible for triggering a payment and also creating the payment record, you could use the fake in-memory implementation to task.
you could use the fake in-memory implementation to task and then have a separate task, for example, the PayBuddy implementation in isolation, and since they both follow the same contract and the same types, you know that it's going to work because you're depending on an abstraction,
you know that it's going to work because you're depending on an abstraction, not on an implementation. With that said, this lesson is already quite big, so let's stop for now and pick up later. Thank you, and I hope you are enjoying the series. See you later.
and I hope you are enjoying the series. See you later. Bye-bye.
