Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

twoheadedboy90's avatar

Need feedback for consuming an external API

Need some feedback if what I'm doing is a clean way in handling requests to an external API. I have an external api that provided a username and password to generate the access_token and here's what I did for the SomeWebServiceAPI class:

Everytime SomeWebserviceAPI gets instantiated it checks via __construct if an access token is available from cache, if not it will send a POST request to the API's auth service and store it in cache and assign it via $this->http->withToken

Is there a better approach to this?

class SomeWebServiceAPI {

	protected $http;

	public function __construct() {

		$url = 'auth';

		$this->http = Http::baseUrl(config('some_web_service.url'));

		$token_name = config('some_webservice.api_token_name');
		$token_expiration = config('some_webservice.api_token_expiration');
		
		$token = Cache::remember($token_name, $token_expiration, function() use($url) {

		    $response = $this->http->post($url,[
		    	'username'	=>	config('some_webservice.username'),
		    	'password'	=>	config('some_webservice.password')
		    ])
		    ->throw()
		    ->json();

		    return Crypt::encryptString($response['token']);
		    
		});

		$this->http->withToken(Crypt::decryptString($token));

	}

	public function getOrderInfo($id) {

		$url = 'orders/'.$id;

		// convert to a DTO
		return $this->http
				->get($url)
				->throw()
				->json();

	}

}
0 likes
3 replies
rodrigo.pedra's avatar
Level 56

I avoid having side-effects on an object's constructor, so I'd move the HTTP client construct to a different method, and accept the API parameters on the constructor:

<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;

class SomeWebServiceAPI
{
    public function __construct(
        private readonly string $baseUrl,
        private readonly string $tokenName,
        private readonly int $tokenExpiration,
        private readonly string $username,
        private readonly string $password,
    ) {
    }

    public function allOrders(): array
    {
        return $this->apiClient()
            ->withToken($this->apiToken())
            ->get('/orders')
            ->json();
    }

    public function orderInfo($id): array
    {
        return $this->apiClient()
            ->withToken($this->apiToken())
            ->get('/orders/' . $id)
            ->json();
    }

    private function apiClient(): PendingRequest
    {
        return Http::baseUrl($this->baseUrl)->throw();
    }

    private function apiToken(): string
    {
        $token = Cache::remember($this->tokenName, $this->tokenExpiration, function () {
            $response = $this->apiClient()->post('/auth', [
                'username' => $this->username,
                'password' => $this->password,
            ])->json();

            return Crypt::encryptString($response['token']);
        });

        return Crypt::decryptString($token);
    }
}

Note individual enpoint methods get very slim, and as you suggest in the comment, I would parse the response and return some sort of DTO or value object instead of the raw array from the response.

Also note I am using PHP 8.1 promoted constructor properties. Read about how it works on PHP docs:

To tell the container how to fill the constructor properties, you need to add contextual bindings in a Service Provider's register method, like so:

<?php

namespace App\Providers;

use App\Services\SomeWebServiceAPI;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->when(SomeWebServiceAPI::class)
            ->needs('$baseUrl')
            ->giveConfig('some_web_service.url');

        $this->app->when(SomeWebServiceAPI::class)
            ->needs('$tokenName')
            ->giveConfig('some_webservice.api_token_name');

        $this->app->when(SomeWebServiceAPI::class)
            ->needs('$tokenExpiration')
            ->giveConfig('some_webservice.api_token_expiration');

        $this->app->when(SomeWebServiceAPI::class)
            ->needs('$username')
            ->giveConfig('some_webservice.username');

        $this->app->when(SomeWebServiceAPI::class)
            ->needs('$password')
            ->giveConfig('some_webservice.password');
    }

    public function boot()
    {
        //
    }
}

This way when you need the SomeWebServiceAPI somewhere, you can inject it on a controller's method signature, or use the resolve() helper, and the container will auto-wire the correct values from your config to the service class.

Read more about contextual bind on Laravel docs:

And more about how Laravel's container resolution works also in its docs:

An advantage of avoiding calling the config() helper inside classes is making it easier to reuse the class with different values, for example when you want to test it, without messing with swapping config values.

Also if you want to reorganize your config files as your system grows, you will have less places to search for usage.

Hope this helps =)

3 likes

Please or to participate in this conversation.