Files
michaelschiemer/src/Framework/HttpClient/CurlHttpClient.php
Michael Schiemer 3ed2685e74 feat: add comprehensive framework features and deployment improvements
Major additions:
- Storage abstraction layer with filesystem and in-memory implementations
- Gitea API integration with MCP tools for repository management
- Console dialog mode with interactive command execution
- WireGuard VPN DNS fix implementation and documentation
- HTTP client streaming response support
- Router generic result type
- Parameter type validator for framework core

Framework enhancements:
- Console command registry improvements
- Console dialog components
- Method signature analyzer updates
- Route mapper refinements
- MCP server and tool mapper updates
- Queue job chain and dependency commands
- Discovery tokenizer improvements

Infrastructure:
- Deployment architecture documentation
- Ansible playbook updates for WireGuard client regeneration
- Production environment configuration updates
- Docker Compose local configuration updates
- Remove obsolete docker-compose.yml (replaced by environment-specific configs)

Documentation:
- PERMISSIONS.md for access control guidelines
- WireGuard DNS fix implementation details
- Console dialog mode usage guide
- Deployment architecture overview

Testing:
- Multi-purpose attribute tests
- Gitea Actions integration tests (typed and untyped)
2025-11-04 20:39:48 +01:00

276 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\HttpClient;
use App\Framework\Http\HeaderManipulator;
use App\Framework\Http\Headers;
use App\Framework\Http\Status;
use App\Framework\HttpClient\Curl\Handle;
use App\Framework\HttpClient\Exception\CurlExecutionFailed;
use App\Framework\HttpClient\Exception\CurlNotInitialized;
final readonly class CurlHttpClient implements HttpClient
{
public function __construct(
private CurlResponseParser $responseParser = new CurlResponseParser(),
private AuthenticationHandler $authenticationHandler = new AuthenticationHandler(),
private CurlRequestBuilder $requestBuilder = new CurlRequestBuilder(),
) {
}
/**
* @throws CurlExecutionFailed
*/
public function send(ClientRequest $request): ClientResponse
{
// Initialize OOP curl handle
$handle = new Handle();
try {
// Build options using HandleOption enum
$options = $this->requestBuilder->buildOptions($request);
// Handle authentication
if ($request->options->auth !== null) {
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
if ($authResult->headers !== $request->headers) {
$updatedRequest = $request->with(['headers' => $authResult->headers]);
$options = $this->requestBuilder->buildOptions($updatedRequest);
}
if (! empty($authResult->curlOptions)) {
$options = array_replace($options, $authResult->curlOptions);
}
}
// Set all options using fluent API
$handle->setOptions($options);
// Execute request (automatically uses CURLOPT_RETURNTRANSFER)
$rawResponse = $handle->fetch();
// Parse response using underlying CurlHandle resource
return $this->responseParser->parse($rawResponse, $handle->getResource());
} catch (\Throwable $e) {
// Wrap any exception in CurlExecutionFailed for backward compatibility
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Send request with streaming response to destination
*
* Streams HTTP response directly to a writable stream resource.
* Useful for large file downloads without loading entire response into memory.
*
* @param ClientRequest $request HTTP request to send
* @param resource $destination Writable stream resource (e.g., fopen('file.txt', 'w'))
* @return StreamingResponse Response with headers and status, but no body (streamed to destination)
* @throws CurlExecutionFailed If request execution fails
*/
public function sendStreaming(ClientRequest $request, $destination): StreamingResponse
{
if (! is_resource($destination)) {
throw new \InvalidArgumentException('Destination must be a valid stream resource');
}
$handle = new Handle();
try {
// Build options using HandleOption enum
$options = $this->requestBuilder->buildOptions($request);
// Remove CURLOPT_RETURNTRANSFER (we're streaming to destination)
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
// Handle authentication
if ($request->options->auth !== null) {
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
if ($authResult->headers !== $request->headers) {
$updatedRequest = $request->with(['headers' => $authResult->headers]);
$options = $this->requestBuilder->buildOptions($updatedRequest);
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
}
if (! empty($authResult->curlOptions)) {
$options = array_replace($options, $authResult->curlOptions);
}
}
// Enable header capture for streaming
$headerBuffer = '';
$headerFunction = function ($ch, $header) use (&$headerBuffer) {
$headerBuffer .= $header;
return strlen($header);
};
$options[\App\Framework\HttpClient\Curl\HandleOption::HeaderFunction->value] = $headerFunction;
// Set all options
$handle->setOptions($options);
// Execute and stream directly to destination
$handle->execute($destination);
// Parse response headers from buffer
$statusCode = $handle->getInfo(\App\Framework\HttpClient\Curl\Info::ResponseCode);
$status = \App\Framework\Http\Status::from((int) $statusCode);
$headers = $this->responseParser->parseHeaders($headerBuffer);
// Get bytes written (if available)
$bytesWritten = (int) ($handle->getInfo(\App\Framework\HttpClient\Curl\Info::SizeDownload) ?: 0);
return new StreamingResponse(
status: $status,
headers: $headers,
bytesWritten: $bytesWritten
);
} catch (\Throwable $e) {
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Send request with streaming body from source
*
* Streams HTTP request body from a readable stream resource.
* Useful for large file uploads without loading entire file into memory.
*
* @param ClientRequest $request HTTP request to send (body will be replaced by stream)
* @param resource $source Readable stream resource (e.g., fopen('file.txt', 'r'))
* @param int|null $contentLength Content-Length in bytes (null for chunked transfer)
* @return ClientResponse HTTP response
* @throws CurlExecutionFailed If request execution fails
*/
public function sendStreamingUpload(ClientRequest $request, $source, ?int $contentLength = null): ClientResponse
{
if (! is_resource($source)) {
throw new \InvalidArgumentException('Source must be a valid stream resource');
}
$handle = new Handle();
try {
// Build options using HandleOption enum
$options = $this->requestBuilder->buildOptions($request);
// Remove CURLOPT_POSTFIELDS (we're using stream)
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
// Set streaming upload options
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
if ($contentLength !== null) {
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
}
// Handle authentication
if ($request->options->auth !== null) {
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
if ($authResult->headers !== $request->headers) {
$updatedRequest = $request->with(['headers' => $authResult->headers]);
$options = array_replace($options, $this->requestBuilder->buildOptions($updatedRequest));
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
if ($contentLength !== null) {
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
}
}
if (! empty($authResult->curlOptions)) {
$options = array_replace($options, $authResult->curlOptions);
}
}
// Set all options
$handle->setOptions($options);
// Execute request (automatically uses CURLOPT_RETURNTRANSFER)
$rawResponse = $handle->fetch();
// Parse response
return $this->responseParser->parse($rawResponse, $handle->getResource());
} catch (\Throwable $e) {
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
}
}
/*
// URL mit Query-Parametern verarbeiten
$url = $request->url;
if (!empty($request->options->query)) {
$separator = str_contains($url, '?') ? '&' : '?';
$url .= $separator . http_build_query($request->options->query);
}
$options = [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => $request->method->value,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => $request->options->timeout,
CURLOPT_CONNECTTIMEOUT => $request->options->connectTimeout,
CURLOPT_FOLLOWLOCATION => $request->options->followRedirects,
CURLOPT_MAXREDIRS => $request->options->maxRedirects,
CURLOPT_SSL_VERIFYPEER => $request->options->verifySsl,
CURLOPT_SSL_VERIFYHOST => $request->options->verifySsl ? 2 : 0,
];
// User-Agent setzen wenn vorhanden
if ($request->options->userAgent !== null) {
$options[CURLOPT_USERAGENT] = $request->options->userAgent;
}
// Proxy setzen wenn vorhanden
if ($request->options->proxy !== null) {
$options[CURLOPT_PROXY] = $request->options->proxy;
}
// Authentifizierung einrichten
if ($request->options->auth !== null) {
$this->setupAuthentication($ch, $request->options->auth, $request->headers);
}
// Request-Body verarbeiten
if ($request->body !== '') {
$options[CURLOPT_POSTFIELDS] = $request->body;
}
// Headers formatieren und setzen
if (count($request->headers->all()) > 0) {
$options[CURLOPT_HTTPHEADER] = HeaderManipulator::formatForCurl($request->headers);
}
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
if ($raw === false) {
throw new CurlExecutionFailed(curl_error($ch), curl_errno($ch));
}
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headersRaw = substr($raw, 0, $headerSize);
$body = substr($raw, $headerSize);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headers = HeaderManipulator::fromString($headersRaw);
return new ClientResponse(
status: Status::from($status),
headers: $headers,
body: $body
);
}
*/
}