Just a starter place for one off clients that need to talk to an API. Sometimes there might even be a library for this but you want to keep it simple.
In this example we will talk to https://cicero.azavea.com/docs
Here is what I would call the client:
<?php
namespace App\Officials;
use App\Exceptions\CiceroClientException;
use App\MailGun\MailGunException;
use App\Officials\Dtos\OfficialsApiDto;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
class CiceroClient
{
public $url = 'https://cicero.azavea.com/v3.1';
public PendingRequest $client;
public function fullUrl($string): string
{
return sprintf('%s/%s',
$this->url, str($string)
->whenStartsWith('/', function ($item) {
return str($item)->replaceFirst('/', '');
})
->toString());
}
public function client(): PendingRequest
{
$token = config('services.cicero.token');
if (! $token) {
throw new CiceroClientException('Token missing');
}
$this->client = Http::withBasicAuth(
'key', $token
);
return $this->client;
}
public function getClient() : PendingRequest {
return $this->client();
}
public function searchOfficial(
string $firstName,
string $lastName
) : OfficialsApiDto {
$uri = sprintf('/official');
$fullUrl = $this->fullUrl($uri);
$response = $this->getClient()->get($fullUrl, [
'first_name' => $firstName,
'last_name' => $lastName,
'valid_on_or_after' => now()->format("Y-m-d")
]);
$response = $this->getResponse($response);
return new OfficialsApiDto($response);
}
public function getResponse(Response $results): array
{
if ($results->failed()) {
logger($results->body());
throw new CiceroClientException('Error with response see logs');
}
return $results->json();
}
}
Note I return a DTO https://github.com/spatie/data-transfer-object to make the return value more structured.
The searchOfficial is all I really will do with this api. I make sure to add "cicero" to may "services.php". And in my "phpunit.xml"
<php>
<env name="CICERO_TOKEN" value="foobar"/>
<env name="APP_ENV" value="testing"/>
I add this one for testing so I never worry about hitting the real api.
I can then make this test to prove it is working:
<?php
namespace Tests\Feature;
use Facades\App\Officials\CiceroClient;
use App\Officials\Dtos\OfficialsApiDto;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
use Illuminate\Http\Client\Request;
class CiceroClientTest extends TestCase
{
public function test_client() {
$data = get_fixture('cicero_official_results.json');
Http::fake([
'cicero.azavea.com/*' => Http::response($data, 200)
]);
$response = CiceroClient::searchOfficial(
"Bob",
"Belcher"
);
$this->assertInstanceOf(OfficialsApiDto::class, $response);
Http::assertSentCount(1);
Http::assertSent(function(Request $request) {
return $request['first_name'] === 'Bob' && $request['last_name'] === 'Belcher' ;
});
}
}
Now I can use this but you can see I use "use Facades\App\Officials\CiceroClient;" but I am about to make it a real Facade so I can also mock the Client.
<?php
namespace App\Officials;
use App\Officials\Dtos\OfficialsApiDto;
class CiceroMockClient
{
public function searchOfficial(
string $firstName,
string $lastName
) : OfficialsApiDto {
$data = get_fixture("cicero_official_results.json");
return new OfficialsApiDto($data);
}
}
And a Facade version
<?php
namespace App\Officials;
class CiceroClientFacade extends \Illuminate\Support\Facades\Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor(): string
{
return 'cicero_client';
}
}
Then in the "app/Providers/AppServiceProvider.php"
<?php
namespace App\Providers;
use App\MailGun\MailgunClient;
use App\Officials\CiceroClient;
use App\Officials\CiceroMockClient;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$this->app->bind('mailgun', fn () => new MailgunClient());
$this->app->bind('cicero_client', function() {
if(config("services.cicero.mock")) {
return new CiceroMockClient();
}
return new CiceroClient();
});
}
}
And in the "config/services.php"
'cicero' => [
'token' => env("CICERO_TOKEN"),
'mock' => env("CICERO_MOCK"),
]
This is just nice in the UI when I want to see "results" but not hit the real API.
One more test to see the facade working and check for typos:
<?php
namespace Tests\Feature;
use App\Officials\CiceroClientFacade;
use App\Officials\Dtos\OfficialsApiDto;
use Illuminate\Support\Facades\Config;
use Tests\TestCase;
class CiceroClientFacadeTest extends TestCase
{
public function test_facade_working() {
Config::set("services.cicero.mock", true);
$response = CiceroClientFacade::searchOfficial(
"Bob",
"Belcher"
);
$this->assertInstanceOf(OfficialsApiDto::class, $response);
}
}