Skip to content

Instantly share code, notes, and snippets.

@recca0120
Created December 30, 2024 03:59
Show Gist options
  • Save recca0120/2531be9f81f3904f4e7f9e9b60eb6d2d to your computer and use it in GitHub Desktop.
Save recca0120/2531be9f81f3904f4e7f9e9b60eb6d2d to your computer and use it in GitHub Desktop.
Laravel HMVC
<?php
namespace App\Services;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Validation\ValidationException;
use RuntimeException;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\Response;
class InternalClient
{
private const HEADER_KEY = 'INTERNAL-CLIENT';
private ?Authenticatable $user = null;
public function __construct(private readonly Container $container, private Request $request)
{
}
public static function isInternal(Request $request): bool
{
return (bool) $request->headers->get(self::HEADER_KEY) === true;
}
public function setRequest(Request $request): static
{
$this->request = $request;
return $this;
}
public function user(?Authenticatable $user): static
{
$this->user = $user;
return $this;
}
public function get(string $uri, array $headers = []): Response|JsonResponse
{
return $this->run('GET', $uri, [], $headers);
}
public function getJson(string $uri, array $headers = []): Response|JsonResponse
{
return $this->json('GET', $uri, [], $headers);
}
public function post(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->run('POST', $uri, $data, $headers);
}
public function postJson(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->json('POST', $uri, $data, $headers);
}
public function put(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->run('PUT', $uri, $data, $headers);
}
public function putJson(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->json('PUT', $uri, $data, $headers);
}
public function delete(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->run('DELETE', $uri, $data, $headers);
}
public function deleteJson(string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->json('DELETE', $uri, $data, $headers);
}
private function createRequest(string $uri, string $method, array $data = [], array $headers = []): Request
{
$parsed = parse_url($uri);
$queryString = $parsed['query'] ?? '';
parse_str($queryString, $query);
$request = $this->request->duplicate();
$request->server->add([
...$this->request->server(),
'REQUEST_METHOD' => $method,
'REQUEST_URI' => $parsed['path'],
'QUERY_STRING' => $parsed['query'] ?? '',
'HTTP_'.str_replace('-', '_', self::HEADER_KEY) => true,
]);
$request->headers->add([...$headers, self::HEADER_KEY => true]);
$request->query->replace($query);
$request->request->replace($data);
if ($request->isJson()) {
$request->setJson(new InputBag($data));
}
if ($this->user) {
$request->setUserResolver(fn () => $this->user);
}
return $request;
}
private function run(string $method, string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
$request = $this->createRequest($uri, $method, $data, $headers);
$route = $this->findRoute($method, $request);
return $this->throwIfResponseHasErrors(
$this->container->call(
[$route->getController(), $route->getActionMethod()],
[...$route->parameters(), 'request' => $request]
)
);
}
private function json(string $method, string $uri, array $data = [], array $headers = []): Response|JsonResponse
{
return $this->run($method, $uri, $data, [
...$headers,
'CONTENT_TYPE' => 'application/json',
'Accept' => 'application/json',
]);
}
private function throwIfResponseHasErrors(Response|JsonResponse $response): Response|JsonResponse
{
$data = $response->getData(true);
if (array_key_exists('errors', $data) && $response->status() === 422) {
throw ValidationException::withMessages($data['errors']);
}
if (array_key_exists('status', $data) && $data['status'] === 'fail') {
$errors = $data['error'] ?? $data['errors'];
if (! is_array($errors)) {
$errors = ['error' => $errors];
}
throw ValidationException::withMessages($errors);
}
return $response;
}
private function findRoute(string $method, Request $request): Route
{
$routes = RouteFacade::getRoutes();
foreach ($routes->get($method) as $route) {
if ($route->matches($request)) {
return $route->bind($request);
}
}
throw new RuntimeException('route not found');
}
}
<?php
namespace Tests\Feature\Services;
use App\Services\InternalClient;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Testing\TestResponse;
use Illuminate\Validation\ValidationException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Tests\TestCase;
/**
* @link InternalClient
*/
class InternalClientTest extends TestCase
{
use RefreshDatabase;
private InternalClient $client;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->createOne();
$this->client = $this->app->make(InternalClient::class);
Route::get('/internal-client/is-internal', [InternalClientTestController::class, 'isInternal']);
Route::get('/internal-client/validate-error', [InternalClientTestController::class, 'validateError']);
Route::get('/internal-client/validate-errors', [InternalClientTestController::class, 'validateErrors']);
Route::post('/internal-client/json', [InternalClientTestController::class, 'json']);
Route::get('/internal-client/user', [InternalClientTestController::class, 'user']);
Route::get('/internal-client', [InternalClientTestController::class, 'index']);
Route::post('/internal-client', [InternalClientTestController::class, 'create']);
Route::put('/internal-client/{id}', [InternalClientTestController::class, 'update']);
Route::get('/internal-client/{id}', [InternalClientTestController::class, 'show']);
Route::delete('/internal-client/{id}', [InternalClientTestController::class, 'destroy']);
}
public function test_validate_error(): void
{
try {
$this->client->getJson('/internal-client/validate-error');
} catch (ValidationException $e) {
self::assertEquals(['error' => ['error']], $e->errors());
}
}
public function test_validate_errors(): void
{
try {
$this->client->getJson('/internal-client/validate-errors');
} catch (ValidationException $e) {
self::assertEquals(['foo' => ['bar']], $e->errors());
}
}
public function test_get_with_query(): void
{
$response = new TestResponse($this->client->get('/internal-client?foo=bar'));
$response->assertJson([
'status' => 'success',
'foo' => 'bar',
]);
}
public function test_get_json_with_query(): void
{
$response = new TestResponse($this->client->getJson('/internal-client?foo=bar'));
$response->assertJson([
'status' => 'success',
'foo' => 'bar',
]);
}
public function test_get_with_id_and_header(): void
{
$response = new TestResponse($this->client->get('/internal-client/1234?foo=bar', [
'header' => 'header-value',
]));
$response->assertJson([
'status' => 'success',
'id' => '1234',
'header' => 'header-value',
'foo' => 'bar',
]);
}
public function test_get_json_with_id_and_header(): void
{
$response = new TestResponse($this->client->getJson('/internal-client/1234?foo=bar', [
'header' => 'header-value',
]));
$response->assertJson([
'status' => 'success',
'id' => '1234',
'header' => 'header-value',
'foo' => 'bar',
]);
}
public function test_post(): void
{
$response = new TestResponse($this->client->post('/internal-client/?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'foo' => 'bar',
'fuzz' => 'buzz',
]);
}
public function test_post_json(): void
{
$response = new TestResponse($this->client->postJson('/internal-client/?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'foo' => 'bar',
'fuzz' => 'buzz',
]);
}
public function test_put(): void
{
$response = new TestResponse($this->client->put('/internal-client/1234?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'id' => '1234',
'foo' => 'bar',
'fuzz' => 'buzz',
]);
}
public function test_put_json(): void
{
$response = new TestResponse($this->client->putJson('/internal-client/1234?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'id' => '1234',
'foo' => 'bar',
'fuzz' => 'buzz',
]);
}
public function test_delete(): void
{
$response = new TestResponse($this->client->delete('/internal-client/1234?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'id' => '1234',
]);
}
public function test_delete_json(): void
{
$response = new TestResponse($this->client->deleteJson('/internal-client/1234?foo=bar', ['fuzz' => 'buzz']));
$response->assertJson([
'status' => 'success',
'id' => '1234',
]);
}
public function test_get_user(): void
{
$response = new TestResponse($this->client->user($this->user)->get('/internal-client/user'));
$response->assertJson([
'status' => 'success',
'id' => $this->user->id,
'username' => $this->user->username,
]);
}
public function test_is_internal(): void
{
$response = new TestResponse($this->client->get('/internal-client/is-internal'));
$response->assertJson(['is_internal' => true]);
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function test_json(): void
{
/** @var Request $request */
$request = $this->app->get('request')->duplicate();
$request->headers->set('content-type', 'application/json');
$this->client->setRequest($request);
$response = new TestResponse(
$this->client->post(
'/internal-client/json?foo=bar',
['fuzz' => 'buzz'],
['content-type' => 'application/json']
)
);
$response->assertJson([
'status' => 'success',
'foo' => 'bar',
'fuzz' => 'buzz',
]);
}
}
class InternalClientTestController
{
public function index(Request $request): JsonResponse
{
return response()->json([
'status' => 'success',
...$request->all(),
]);
}
public function show(Request $request, $id): JsonResponse
{
return response()->json([
'status' => 'success',
'id' => $id,
'header' => $request->headers->get('header'),
...$request->all(),
]);
}
public function create(Request $request): JsonResponse
{
return response()->json([
'status' => 'success',
...$request->all(),
]);
}
public function update(Request $request, $id): JsonResponse
{
return response()->json([
'status' => 'success',
'id' => $id,
...$request->all(),
]);
}
public function destroy($id): JsonResponse
{
return response()->json([
'status' => 'success',
'id' => $id,
]);
}
public function user(Request $request): JsonResponse
{
return response()->json([
'status' => 'success',
'id' => $request->user()->id,
'username' => $request->user()->username,
]);
}
public function json(Request $request): JsonResponse
{
return response()->json([
'status' => 'success',
...$request->all(),
]);
}
public function isInternal(Request $request): JsonResponse
{
return response()->json(['is_internal' => InternalClient::isInternal($request)]);
}
public function validateError(): JsonResponse
{
return response()->json([
'status' => 'fail',
'error' => 'error',
]);
}
public function validateErrors(): JsonResponse
{
return response()->json([
'status' => 'fail',
'errors' => [
'foo' => ['bar'],
],
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment