Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,29 +232,38 @@ private function withRetries(callable $callback): mixed
*
* @param string $url
* @param string $method
* @param array<string>|array<string, mixed> $body
* @param array<string>|array<string, mixed>|FormData|null $body
* @param array<string, mixed> $query
* @param ?callable $chunks Optional callback function that receives a Chunk object
* @return Response
* @throws Exception
*/
public function fetch(
string $url,
string $method = self::METHOD_GET,
?array $body = [],
mixed $body = [],
?array $query = [],
?callable $chunks = null,
): Response {
if (!in_array($method, [self::METHOD_PATCH, self::METHOD_GET, self::METHOD_CONNECT, self::METHOD_DELETE, self::METHOD_POST, self::METHOD_HEAD, self::METHOD_OPTIONS, self::METHOD_PUT, self::METHOD_TRACE])) {
throw new Exception("Unsupported HTTP method");
}

if (isset($this->headers['content-type']) && $body !== null) {
$body = match ($this->headers['content-type']) {
self::CONTENT_TYPE_APPLICATION_JSON => $this->jsonEncode($body),
self::CONTENT_TYPE_APPLICATION_FORM_URLENCODED, self::CONTENT_TYPE_MULTIPART_FORM_DATA => self::flatten($body),
self::CONTENT_TYPE_GRAPHQL => $body[0],
default => $body,
};
if ($body !== null) {
if ($body instanceof FormData) {
$this->headers['content-type'] = $body->getContentType();
$body = $body->build();

} elseif (isset($this->headers['content-type'])) {
$body = match ($this->headers['content-type']) {
self::CONTENT_TYPE_APPLICATION_JSON => $this->jsonEncode($body),
self::CONTENT_TYPE_APPLICATION_FORM_URLENCODED,
self::CONTENT_TYPE_MULTIPART_FORM_DATA => self::
($body),
self::CONTENT_TYPE_GRAPHQL => $body[0],
default => $body,
};
}
}

$formattedHeaders = array_map(function ($key, $value) {
Expand Down
191 changes: 191 additions & 0 deletions src/FormData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

namespace Utopia\Fetch;

class FormData
{
/**
* @var string
*/
private string $boundary;

/**
* @var array<array<string, mixed>>
*/
private array $fields = [];

/**
* @var array<array<string, mixed>>
*/
private array $files = [];

/**
* Constructor
*/
public function __construct()
{
// Generate a unique boundary
$this->boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(16));
}

/**
* Add a text field to the multipart request
*
* @param string $name
* @param string $value
* @param array<string, string> $headers Optional additional headers
* @return self
*/
public function addField(string $name, string $value, array $headers = []): self
{
$this->fields[] = [
'name' => $name,
'value' => $value,
'headers' => $headers
];

return $this;
}

/**
* Add a file to the multipart request
*
* @param string $name Field name
* @param string $filePath Path to the file
* @param string|null $fileName Custom filename (optional)
* @param string|null $mimeType Custom mime type (optional)
* @param array<string, string> $headers Optional additional headers
* @return self
* @throws \Exception If file doesn't exist or isn't readable
*/
public function addFile(
string $name,
string $filePath,
?string $fileName = null,
?string $mimeType = null,
array $headers = []
): self {
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new Exception("File doesn't exist or isn't readable: {$filePath}");
}

$this->files[] = [
'name' => $name,
'path' => $filePath,
'filename' => $fileName ?? basename($filePath),
'mime_type' => $mimeType ?? mime_content_type($filePath) ?: 'application/octet-stream',
'headers' => $headers
];

return $this;
}

/**
* Add file content directly to the multipart request
*
* @param string $name Field name
* @param string $content File content
* @param string $fileName Filename to use
* @param string|null $mimeType Custom mime type (optional)
* @param array<string, string> $headers Optional additional headers
* @return self
*/
public function addContent(
string $name,
string $content,
string $fileName,
?string $mimeType = null,
array $headers = []
): self {
$this->files[] = [
'name' => $name,
'content' => $content,
'filename' => $fileName,
'mime_type' => $mimeType ?? 'application/octet-stream',
'headers' => $headers
];

return $this;
}

/**
* Build request body based on content type
*
* @return string
*/
public function build(): string
{
// If no files, use application/x-www-form-urlencoded format
if (empty($this->files)) {
$formData = [];
foreach ($this->fields as $field) {
$formData[$field['name']] = $field['value'];
}
return http_build_query($formData);
}

// Otherwise, build multipart/form-data
$body = '';

// Add fields
foreach ($this->fields as $field) {
$body .= "--{$this->boundary}\r\n";
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n";

// Add custom headers
foreach ($field['headers'] as $key => $value) {
$body .= "{$key}: {$value}\r\n";
}

$body .= "\r\n";
$body .= $field['value'] . "\r\n";
}

// Add files
foreach ($this->files as $file) {
$body .= "--{$this->boundary}\r\n";
$body .= "Content-Disposition: form-data; name=\"{$file['name']}\"; filename=\"{$file['filename']}\"\r\n";
$body .= "Content-Type: {$file['mime_type']}\r\n";

// Add custom headers
foreach ($file['headers'] as $key => $value) {
$body .= "{$key}: {$value}\r\n";
}

$body .= "\r\n";

// Add file content
if (isset($file['content'])) {
$body .= $file['content'];
} else {
$body .= file_get_contents($file['path']);
}

$body .= "\r\n";
}

// End boundary
$body .= "--{$this->boundary}--\r\n";

return $body;
}

public function setBoundary(string $boundary): void
{
$this->boundary = $boundary;
}

/**
* Get content type with boundary
*
* @return string
*/
public function getContentType(): string
{
if (empty($this->files)) {
return 'application/x-www-form-urlencoded';
}

return 'multipart/form-data; boundary=' . $this->boundary;
}
}
Loading