Skip to content

Commit

Permalink
Create API client classes (#4)
Browse files Browse the repository at this point in the history
- ControlD.php will make HTTP requests
- ControlDFactory.php creates the client class
- Bind the ControlD client class and build it with the factory in ControldServiceProvider.php. Used for dependency injection
- Add tests for the factory
- Use TestMiddleware.php to assert all methods
- Define API endpoint and secret bearer token in .env and config
- Installed guzzlehttp/guzzle to support the underlying Laravel HTTP client
- Set up test coverage in run-tests.yml
- Set minimum test coverage to 100 percent
- Fix unused autoload path in composer.json
  • Loading branch information
rapkis authored Aug 28, 2023
1 parent d30a3f8 commit 6d72904
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CONTROL_D_API_URL=
CONTROL_D_API_SECRET=
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: none
coverage: xdebug

- name: Setup problem matchers
run: |
Expand All @@ -48,4 +48,4 @@ jobs:
run: composer show -D

- name: Execute tests
run: vendor/bin/pest --ci
run: XDEBUG_MODE=coverage php ./vendor/bin/pest --ci --coverage --min=100 --do-not-cache-result
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ phpstan.neon
testbench.yaml
vendor
node_modules
.env
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
],
"require": {
"php": "^8.1",
"spatie/laravel-package-tools": "^1.14.0",
"illuminate/contracts": "^10.0"
"guzzlehttp/guzzle": "^7.7",
"illuminate/contracts": "^10.0",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
"laravel/pint": "^1.0",
Expand All @@ -34,8 +35,7 @@
},
"autoload": {
"psr-4": {
"Rapkis\\Controld\\": "src/",
"Rapkis\\Controld\\Database\\Factories\\": "database/factories/"
"Rapkis\\Controld\\": "src/"
}
},
"autoload-dev": {
Expand Down Expand Up @@ -80,4 +80,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
3 changes: 2 additions & 1 deletion config/controld.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

// config for Rapkis/Controld
return [

'url' => env('CONTROL_D_API_URL'),
'secret' => env('CONTROL_D_API_SECRET'),
];
14 changes: 14 additions & 0 deletions src/Api/ControlD.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Rapkis\Controld\Api;

use Illuminate\Http\Client\PendingRequest;

class ControlD
{
public function __construct(private PendingRequest $request)

Check failure on line 11 in src/Api/ControlD.php

View workflow job for this annotation

GitHub Actions / phpstan

Property Rapkis\Controld\Api\ControlD::$request is never read, only written.
{
}
}
31 changes: 31 additions & 0 deletions src/Api/ControlDFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Rapkis\Controld\Api;

use Illuminate\Config\Repository;
use Illuminate\Http\Client\PendingRequest;

class ControlDFactory
{
public function __construct(private PendingRequest $request, private Repository $config)
{
}

public function make(): ControlD
{
$this->request
->asJson()
->acceptJson()
->baseUrl($this->config->get('controld.url'))
->withToken($this->config->get('controld.secret'))
->retry(3, 250, new RetryCallback());

foreach ($this->config->get('controld.middleware') ?? [] as $middleware) {
$this->request->withMiddleware(app($middleware));
}

return new ControlD($this->request);
}
}
16 changes: 16 additions & 0 deletions src/Api/RetryCallback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Rapkis\Controld\Api;

use Illuminate\Http\Client\ConnectionException;
use Throwable;

class RetryCallback
{
public function __invoke(Throwable $exception): bool
{
return $exception instanceof ConnectionException || $exception->getCode() >= 500;
}
}
11 changes: 11 additions & 0 deletions src/ControldServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Rapkis\Controld;

use Illuminate\Contracts\Foundation\Application;
use Rapkis\Controld\Api\ControlD;
use Rapkis\Controld\Api\ControlDFactory;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

Expand All @@ -18,4 +21,12 @@ public function configurePackage(Package $package): void
->name('laravel-controld')
->hasConfigFile();
}

public function boot()
{
$this->app->bind(
ControlD::class,
fn (Application $app) => $app->make(ControlDFactory::class)->make(),
);
}
}
37 changes: 37 additions & 0 deletions tests/Api/ControlDFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use Illuminate\Config\Repository;
use Illuminate\Http\Client\PendingRequest;
use Rapkis\Controld\Api\ControlDFactory;
use Rapkis\Controld\Api\RetryCallback;
use Rapkis\Controld\Tests\Api\TestMiddleware;

it('creates an api client', function () {
$config = app(Repository::class);
$request = $this->createMock(PendingRequest::class);
$factory = new ControlDFactory($request, $config);
app()->bind(TestMiddleware::class, fn () => $this->createStub(TestMiddleware::class));

$config->set([
'controld' => [
'url' => 'example.com',
'secret' => 'bearer_token',
'middleware' => [TestMiddleware::class],
],
]);

$request->expects($this->once())->method('asJson')->willReturnSelf();
$request->expects($this->once())->method('acceptJson')->willReturnSelf();
$request->expects($this->once())->method('baseUrl')->with('example.com')->willReturnSelf();
$request->expects($this->once())->method('withToken')->with('bearer_token')->willReturnSelf();

$request->expects($this->once())->method('withMiddleware')
->with($this->createStub(TestMiddleware::class))
->willReturnSelf();

$request->expects($this->once())->method('retry')
->with(3, 250, new RetryCallback())
->willReturnSelf();

$factory->make();
});
17 changes: 17 additions & 0 deletions tests/Api/RetryCallbackTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

use Illuminate\Http\Client\ConnectionException;
use Rapkis\Controld\Api\RetryCallback;

it('will retry for exception', function (Throwable $exception, bool $willRetry) {
$retry = new RetryCallback();

expect($retry($exception))->toBe($willRetry);
})->with([
[new Exception(), false],
[new Exception('', 500), true],
[new Exception('', 422), false],
[new ConnectionException(), true],
]);
18 changes: 18 additions & 0 deletions tests/Api/TestMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Rapkis\Controld\Tests\Api;

use Closure;
use Psr\Http\Message\RequestInterface;

class TestMiddleware
{
public function __invoke(callable $handler): Closure
{
return function (RequestInterface $request, array $options = []) use ($handler) {
$request->withHeader('test', 'test');

return $handler($request, $options);
};
}
}

0 comments on commit 6d72904

Please sign in to comment.