When you extensively work with certain APIs, like Shopify’s for example, you will end up with bunch of functions that map to API’s endpoints. One of the approaches I have seen so far is to create an API class ShopifyApi and make those functions class methods. So it looks something like the figure below.
I see several problems with this approach. First of all it is not really object oriented design. A bunch of functions wrapped in a class are still a bunch of functions. Second, it violates Single Responsibility principle. ShopifyApi class is obviously trying to do a lot of things. Changes in different areas will require you to change this class. Thirdly, this class is not very testable. It will be hard to test it without hitting an actual API endpoint.
Let’s make some changes to this class. In PHP we have several ways of sending HTTP request. For example, Guzzle library, Curl PHP extension, or just simple streams. A good refactor will be to pass a transport into ShopifyApi class instead of having transport inside. We can leverage Composition design principle here.
By coding to interface we can choose different implementations of the transport and even swap them at run time. Personally I also use couple helper classes to pass data in and out of the transport: Request and Response.
Now we can use the interface to test our code without hitting an actual API endpoint. We just need to create a fake transport class that implements Transport interface and returns a dummy data. We can pass this class to ShopifyApi when running our tests. In this refactor we use Dependency Inversion design principle
Now let’s split up our ShopifyApi “super class”. We can take each responsibility this class handles and split them out into their own classes
This refactor takes advantage of OOP’s Inheritance. Depending on responsibilities, parent class can be either abstract or concrete. Child classes can be as small as containing only one method. Don’t make this fact keep you from refactoring: “Well, if we extract Fulfillment class, it will only have one method, so it is wasteful allocating the whole class to it”. It may be true now, but later on Shopify will change the way they want you to send fulfillments. For example recently Shopify added locations. Now your Fulfillment class has to figure out what location to use when sending a fulfillment. The good thing about it is that you only have to change Fulfillment class. Your client code still uses the same interface and does not know about the changes. You can see how Single Responsibility and Encapsulation pay off in this case.
In multi tenant applications you may have to use different API credentials for different clients. In this case you can create a some kind of credentials provider. So your design may start looking more like this:
Before you start implementing credentials provider, first think if you really need it. Personally I am against unnecessary complexity and use YAGNI. “You aren’t gonna need it” is a principle that states functionality should not be added until necessary. If your app does not support multi tenancy chances are it won’t do it in the future. Multi tenancy requires a different design approach. If your app uses only one set of credentials to connect to Shopify API, keep it simple and don’t develop unnecessary structure to provide credentials to the API client. YAGNI.
As you can see, the design can become quite involved. I suggest using common sense when creating an API client. If your app only needs to sync products from Shopify, the suggested design complexity may not be justified. But if there is a lot of stuff going on and your app uses a lot of endpoints, it makes more sense to have a testable design that can accommodate future changes without requiring much work. The more time you invest in building a software, the longer you expect it to live. This means you should be able to easily adapt it to changing requirements and make necessary updates fast.