Created
December 30, 2024 03:59
-
-
Save recca0120/2531be9f81f3904f4e7f9e9b60eb6d2d to your computer and use it in GitHub Desktop.
Laravel HMVC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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