laravel 🚀
facades 🚀
note2self 🚀
Quick HTTP Client Example

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);
    }
}