Templating API Calls and More: Exploring the Template Method Pattern in PHP

Grzegorz Lasak (Gregster)
10 min readAug 31, 2023

--

Have you ever felt ensnared in a web of code that’s repetitive yet just distinct enough to resist a straightforward refactor? The sort where adding a new class — whether for a third-party API integration, custom workflow, or some other cool thing — feels like déjà vu? It’s the ‘same same, but different’ conundrum.

One way to solve it is using Template Method Pattern — our orchestra’s conductor, ensuring each musical piece (or, in our coding world, class) maintains a consistent rhythm. At the same time, it allows individual instruments (methods) their spotlight moments. In PHP, this translates to crafting a structured skeleton for your classes, enabling child classes to add unique touches without having to redo the entire symphony.

What is the Template Method Pattern?

Technical Definition: A behavioral design pattern that defines the program skeleton in an algorithm but delays some steps to subclasses.

Well… Let’s define it with some analogy to more day-to-day example.

Analogy: Imagine building a house. There’s a general blueprint that dictates the order in which things need to be done — laying the foundation, putting up walls, installing the roof, etc. However, the specifics, like the colour of the walls or the type of roof tiles, can differ based on individual preferences.

In Application: With the Template Method Pattern, our blueprint is the ‘template method.’ It outlines the sequence in which methods (building steps) should be executed. The specifics of each step — like the wall colour or roof type — are then defined in our individual house designs (subclasses).

Why use the Template Method Pattern? Let’s dive into real example.

The code example is based on code used in real app (although heavily modified to cover the topic and not expose sensitive info) which means some of the things (f.ex. usage of Interfaces, VOs etc.) will not be explained in this article.
Code is written in Laravel 10, so some of the methods are provided by it.
Also, please, pay attention to comments in code examples where I have included more descriptions.

Our story’s hero, Dev Greg, was tasked with implementing code to gather data from multiple third-party API providers.
After spending some time on reading very well described documentations for these APIs (we know how that goes), it came out that both require a request to obtain the Token, followed by a second one to fetch data (where Token must be attached).

After completing the initial implementation (first solve the issue, then refactor ;)), he ended up with 2 Services which had almost identical way of autohorisation and authentication while fetching data:
1. Send request with Integration-specific data for authorisation to receive Token
2. Extract Token from response and keep it
3. Send request to fetch data with attached Token and Integration-specific setup (Headers, body request etc.).

These 2 Services contained a lot of repeated code but at the same time there were some Integration-specific caveats which made it impossible to just share methods served from Parent class:

  • Each integration expected different set of keys, some encoded in specific manner, attached in (also different) Headers for the authorization, different URLs, Request types etc.
  • One integration was REST (supporting different paths and URL query parameters), another one was using GraphQL, so request body had to be crafted on very different way but all going with the same Request Method and URL (except authentication).

These 2 points made it already clear that Template Method Pattern will be useful in here since it allows to force the rhythm and flow (like symphony’s conductor) and will allow subclasses to have methods which will prepare requests on expected way without tempering with already established order.

Let’s look into the code which is representing Service Abstract Class orchestrating the flow and forcing methods that subclasses have to implement:

<?php

abstract class AbstractTemplateMethodPatternExampleService
{
protected ?IRequestData $requestData = null; // IRequestData is Interface representing all data to be used in Request (not covered in this article in detail)

protected IntegrationModel $integration; // We need to have Integration set at this level (by subclass). It contains all data required for Integration to get access to data

// Custom Service which we don't cover here. Could be original Guzzle Client instead
public function __construct(
protected GuzzleService $guzzleService = new GuzzleService()
) {}

/**
* Each subclass needs to SET RequestVO because it will contain different
* Body Request, Method and URL Depended on third-party API Provider
* but each has to extend IRequestData (Interface) to make sure that
* Contract is fulfilled before calling some of expected methods
*
* @return IRequestData
* @throws \Exception
*/
final protected function getRequestVO(): IRequestData
{
if ( ! $this->requestData ) {
throw new \Exception('RequestVO is not set!');
}

return $this->requestData;
}

/**
* Force Subclass to Return Data Provider Name which is Enum (from our own list), so it cannot be random string
*
* @return DataProvidersEnum
*/
protected abstract function getDataProviderName(): DataProvidersEnum;

/**
* Each customer credentials are separated model/repository.
* Handle logic of extracting authentication data accordingly
* and return IAuthenticationData so headers can be controlled from one place.
*
* @param IntegrationModel $integration
*
* @return IAuthenticationData
*/
protected abstract function getAuthenticationData(
IntegrationModel $integration
): IAuthenticationData; // it will make sure we can request prepared array with authentication details

/**
* Fetch correct data provider credentials and set as class attributes
* for easier reusing.
*
* Some Integrations do require custom logic before we can use it, so subclass needs to take care of it
*
* @param IntegrationModel $integration
*
* @return $this
*/
protected abstract function setIntegration( IntegrationModel $integration ): self;

/**
* Perform authentication request with data provider system and set all required class attrs.
* Recommended to set customer credentials, too.
* Beside, set f.ex. Bearer Token or any obj needed for authorization request so can be reused.
*
* Since each Integration might have it done on different way, subclass has to handle it. It can still reuse GuzzleService provided via DI
*
* @param IntegrationModel $integration
*
* @return $this
*/
protected abstract function authenticate( IntegrationModel $integration ): self;

/**
* Combine request options with authorization headers.
*
* Each Data Provider requires different setup for Authorization, so subclass needs to take care of that
*
* @param IRequestData $request
*
* @return array
*/
protected abstract function mergeRequestOptionsWithAuthorization(
IRequestData $request
): array;

/**
* Validate access token if issued. Default fallback to false.
*
* Each Integration might have different requirement for checking if Token is valid.
* F.ex. it might be checking time (so we don't needs to make authentication requests if our token is still valid)
*
* @param bool $throwException
*
* @return bool
*/
protected function isAccessTokenValid( bool $throwException = false ): bool
{
return false;
}

protected function getIntegration(): ?IntegrationModel
{
return $this->integration;
}

/**
* Some Exceptions are not supposed to be caught since Controller will take care of them.
* It makes sure to check existance of Request responsible for fetching data,
* preparing and sending request.
* Subclasses should not be able (at least in that scenario) modify it, so it if final class.
*
* @param IRequestData $request
* @return Response|null
*
* @throws DataProviderRequestException
* @throws GuzzleException
*/
final protected function sendRequest( IRequestData $request ): ?Response
{

$this->isAccessTokenValid();

try {
return $this->guzzleService->sendRequest(
$request,
$this->mergeRequestOptionsWithAuthorization($request)
);

} catch ( \Exception $e ) {
throw new DataProviderRequestException($request, $e->getMessage());
}
}

/**
* Our symphony's conductor in here ;)
*
* Each Integration might require some additional steps (f.ex. some integrations required multiple requests
* to gather required data), so allow subclass override it.
*
* It is not private method because it would not be usable by subclasses (at least not directly).
* It is not public method because we don't want to allow it to be used outside of context.
*
* This could be marked as a final class, and it probably should be but some integrations might need to implement some corner cases logic
*
* @param IDebugDataQuery $query
* @return Collection
* @throws GuzzleException
*/
protected function fetchData(
IDebugDataQuery $query,
): ?array {
$integration = $query->getIntegration();
$dataProviderResponse = null;

try {
$response = $this
->authenticate( $integration ) // authenticate is forced to be implemented in subclasses
->sendRequest( $this->getRequestVO() ) // sendRequest is defined in current class but can be extended if needed
->getBody()
->getContents();

$dataProviderResponse = JsonHelper::decodeJson($response);
} catch( DataProviderRequestException ) { // already logged, just proceed
} catch( JsonException $e ) {
LogHelper::logDataProvider(
sprintf(
"Issue %s %s data response for integration [%s]: %s",
$this->getDataProviderName()->value, // request to which Data Provider failed?
(new ReflectionClass($query))->getShortName(), // different queries fetch different data
$integration->getAttribute('id'), // so we know which Integration failed since there are many
$e->getMessage()) // and our message for later debugging
);
}

return $dataProviderResponse;
}
}

Here, I’d like to highlight two main points:

  1. Usage of protected abstract method followed by method signature:
    These methods intentionally lack a body.
    That way we can force subclass to implement protected method (unlike public methods by using Interface) which in that case is desired.
    It is important to note that it is possible only inside of abstract class
  2. Body of protected function fetchData method — that is where Template Method Pattern’s essence is kept:
    -
    We trigger authenticate method which is example of method from 1st point.
    - Next, we call sendRequest method with instance of IRequestData Class as parameter which needs to be provided by subclass.
    - At next stage, we check if Token is valid (validation of Token needs to be performed by subclass)
    - If all is good so far, we mergeRequestOptionsWithAuthorization forced to be implemented by subclass and we send request.

Let’s look into example implementation of a class which will use that Pattern and will implement all must-have methods:

<?php
/**
* Class taking care of authenticating with API provider's system.
*
*/
class MyClassForFetchingDataService extends AbstractDebugDataAuthService
{
/** Object representing Bearer Auth Data with its own logic depended on API Integration. */
protected IBearerAuthenticationData $bearerAuthenticationData;

/**
* Implementation of must-have method to know which Data Provider we are dealing with
* @return DataProvidersEnum
*/
protected function getDataProviderName(): DataProvidersEnum
{
return DataProvidersEnum::SOME_PROVIDER;
}

/**
* Custom method Integration-Specific to build URL
* @param string $customerName
* @return string
*/
public function getAuthUrl(string $customerName): string
{
if ( !($authUrl = env('REQUIRED_AUTH_URL')) ) {
throw new MissingEnvParamException('Auth URL not found!');
}

return str_replace(
'##authUrlHolder##',
$customerName,
$authUrl
);
}

/**
* Object representing Bearer Auth Data -
*
* @param IBearerAuthenticationDataVO $authData
*
* @return $this
*/
protected function setBearerAuthenticationData(
IBearerAuthenticationData $authData
): self {
$this->bearerAuthenticationData = $authData;

return $this;
}

/**
* Let's have option to get that object
*
* @return IBearerAuthenticationData
*/
protected function getBearerAuthenticationData(): ?IBearerAuthenticationData
{
return $this->bearerAuthenticationData;
}

/**
* Implementation of must-have method and custom logic for validating Token
* @param bool $throwException
*
* @return bool
*
* @throws AuthenticationTokenNotIssuedException
*/
protected function isAccessTokenValid( bool $throwException = true ): bool
{
if (
isset($this->bearerAuthenticationData) &&
!$this->getBearerAuthenticationData()->isAccessTokenExpired()
) {
return true;
}

if ( $throwException ) {
throw new AuthenticationTokenNotIssuedException(
$this->getDataProviderName()
);
}

return false;
}

/**
* Implementation of must-have method.
* Integration-specific logic for building Auth Query (custom object, not covered in that article)
* @param IntegrationModel $integration
*
* @return IAuthenticationData
*/
protected function getAuthenticationData( IntegrationModel $integration ): IAuthenticationData
{
return new MyAuthDataQuery(
$integration->getAttribute(MyIntegrationModel::COLUMN_DATA_CLIENT_ID),
$integration->getAttribute(MyIntegrationModel::COLUMN_DATA_SECRET_KEY),
);
}

/**
* Not in factory due to fixed context of column names per provider.
* Validate if we have all required data for authenticating in third-party API Provider
* Set Integration model (for further reference)
*
* @param IntegrationModel $integration
*
* @return $this
*
* @throws MissingAuthCredentialsException
*/
protected function setIntegration( IntegrationModel $integration ): self
{
if (
!empty($integration->getAttribute(MyIntegrationModel::COLUMN_DATA_CLIENT_ID)) &&
!empty($integration->getAttribute(MyIntegrationModel::COLUMN_DATA_SECRET_KEY))
) {
$this->integration = $integration;

return $this;
}

throw new MissingAuthCredentialsException(
$this->getDataProviderName(),
$integration->getAttribute(MyIntegrationModel::COLUMN_ID)
);
}

/**
* Implementation of must-have method.
* Authenticate and obtain Token which will be used later to fetch data
*
* @param IntegrationModel $integration
*
* @return $this
*
* @throws AuthenticationTokenNotIssuedException
* @throws DataProviderAuthenticationException
* @throws GuzzleException
* @throws MissingAuthCredentialsException
* @throws MissingEnvParamException
*/
protected function authenticate(MyIntegrationModel $integration ): self
{
// if token still valid, use it. Create new one otherwise
if ( $this->isAccessTokenValid(false) ) return $this;

$this->setIntegration( $integration );

$url = $this->getAuthUrl($integration->getAttribute('customer_name'));

try {
$response = $this->guzzleService->getClient()->request(
'POST',
$url,
$this
->getAuthenticationData($this->getIntegration())
->getRequestOptionsArray()
);

$decodedResponse = JsonHelper::decodeJson(($response->getBody()->getContents()));

$this->setBearerAuthenticationData(
new IBearerAuthenticationDataVO(
$decodedResponse[IBearerAuthenticationData::PARAM_ACCESS_TOKEN],
$decodedResponse[IBearerAuthenticationData::PARAM_EXPIRES_IN],
)
);
} catch ( \Exception $e ) {
throw new DataProviderAuthenticationException(
$this->getDataProviderName(),
$integration->getAttribute(MyIntegrationModel::COLUMN_ID),
$e->getMessage()
);
}

return $this;
}

/**
* Implementation of must-have method.
* After we are authenticated, this will take care of merging authorization requirements with request data
* to be able to get what we need.
*
* @param IRequestData $request
* @return array
*/
protected function mergeRequestOptionsWithAuthorization( IRequestData $request ): array
{
return array_merge(
$request->getRequestOptionsArray(),
(new MyAuthorizationDataQuery(
$this->getBearerAuthenticationData(),
))->getRequestOptionsArray()
);
}

/**
* Attempt to fetchData which will be triggering orchestrated process coordinated by parent class.
* @return array|null
*/
public function fetchMyDesiredData(): ?array {
return $this->fetchData(new MyDebugDataQuery());
}
}

As we can see, the subclass does implement all ‘must-have’ methods and also includes Integration-specific ones.
Most of the methods are protected due to the fact that I would not like any Controller (or another Service using that one) tempt with the data which right now is being set on specific way, so public methods available in our Service should be the only proxies between our app and third-party API provider.

A few last words on the topic

I hope that the example of API integrations with PHP shows the power of Template Method Pattern.

We learned that by creating a blueprint — much like an architect does for houses — we can lay down the general sequence of events, and then let the specific implementations bring in their unique flavours.
This ensures that the core logic remains unaltered, while specific implementations can decide about ‘colour of the walls, type of tails etc.’

I hope you will find it useful and be able to use that pattern in your projects since implementation of common coding techniques makes life easier for other developers to work with our code.

--

--

Grzegorz Lasak (Gregster)

Mastering the art of PHP, JS and GO on daily basis while working as a Web Developer. Still trying to master the art of adulting.