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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.6.2"
".": "0.6.3"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 21
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml
openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d868ff00b7b07f6b6802b00f22fad531a91a76bb219a634f3f90fe488bd499ba.yml
openapi_spec_hash: 20e9f2fc31feee78878cdf56e46dab60
config_hash: 5509bb7a961ae2e79114b24c381606d4
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.6.3 (2026-04-11)

Full Changelog: [v0.6.2...v0.6.3](https://github.com/CASParser/cas-parser-php/compare/v0.6.2...v0.6.3)

### Bug Fixes

* **client:** properly generate file params ([59582b0](https://github.com/CASParser/cas-parser-php/commit/59582b0bcdd8be588e3f53f062a2ff16d29df00d))

## 0.6.2 (2026-03-17)

Full Changelog: [v0.6.1...v0.6.2](https://github.com/CASParser/cas-parser-php/compare/v0.6.1...v0.6.2)
Expand Down
4 changes: 4 additions & 0 deletions src/Core/Conversion.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public static function dump_unknown(mixed $value, DumpState $state): mixed
}

if (is_object($value)) {
if ($value instanceof FileParam) {
return $value;
}

if (is_a($value, class: ConverterSource::class)) {
return $value::converter()->dump($value, state: $state);
}
Expand Down
63 changes: 63 additions & 0 deletions src/Core/FileParam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace CasParser\Core;

/**
* Represents a file to upload in a multipart request.
*
* ```php
* // From a file on disk:
* $client->files->upload(file: FileParam::fromResource(fopen('data.csv', 'r')));
*
* // From a string:
* $client->files->upload(file: FileParam::fromString('csv data...', 'data.csv'));
* ```
*/
final class FileParam
{
public const DEFAULT_CONTENT_TYPE = 'application/octet-stream';

/**
* @param resource|string $data the file content as a resource or string
*/
private function __construct(
public readonly mixed $data,
public readonly string $filename,
public readonly string $contentType = self::DEFAULT_CONTENT_TYPE,
) {}

/**
* Create a FileParam from an open resource (e.g. from fopen()).
*
* @param resource $resource an open file resource
* @param string|null $filename Override the filename. Defaults to the resource URI basename.
* @param string $contentType override the content type
*/
public static function fromResource(mixed $resource, ?string $filename = null, string $contentType = self::DEFAULT_CONTENT_TYPE): self
{
if (!is_resource($resource)) {
throw new \InvalidArgumentException('Expected a resource, got '.get_debug_type($resource));
}

if (null === $filename) {
$meta = stream_get_meta_data($resource);
$filename = basename($meta['uri'] ?? 'upload');
}

return new self($resource, filename: $filename, contentType: $contentType);
}

/**
* Create a FileParam from a string.
*
* @param string $content the file content
* @param string $filename the filename for the Content-Disposition header
* @param string $contentType override the content type
*/
public static function fromString(string $content, string $filename, string $contentType = self::DEFAULT_CONTENT_TYPE): self
{
return new self($content, filename: $filename, contentType: $contentType);
}
}
60 changes: 47 additions & 13 deletions src/Core/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public static function withSetBody(

if (preg_match('/^multipart\/form-data/', $contentType)) {
[$boundary, $gen] = self::encodeMultipartStreaming($body);
$encoded = implode('', iterator_to_array($gen));
$encoded = implode('', iterator_to_array($gen, preserve_keys: false));
$stream = $factory->createStream($encoded);

/** @var RequestInterface */
Expand Down Expand Up @@ -447,11 +447,18 @@ private static function writeMultipartContent(
): \Generator {
$contentLine = "Content-Type: %s\r\n\r\n";

if (is_resource($val)) {
yield sprintf($contentLine, $contentType ?? 'application/octet-stream');
while (!feof($val)) {
if ($read = fread($val, length: self::BUF_SIZE)) {
yield $read;
if ($val instanceof FileParam) {
$ct = $val->contentType ?? $contentType;

yield sprintf($contentLine, $ct);
$data = $val->data;
if (is_string($data)) {
yield $data;
} else { // resource
while (!feof($data)) {
if ($read = fread($data, length: self::BUF_SIZE)) {
yield $read;
}
}
}
} elseif (is_string($val) || is_numeric($val) || is_bool($val)) {
Expand Down Expand Up @@ -483,17 +490,48 @@ private static function writeMultipartChunk(
yield 'Content-Disposition: form-data';

if (!is_null($key)) {
$name = rawurlencode(self::strVal($key));
$name = str_replace(['"', "\r", "\n"], replace: '', subject: $key);

yield "; name=\"{$name}\"";
}

// File uploads require a filename in the Content-Disposition header,
// e.g. `Content-Disposition: form-data; name="file"; filename="data.csv"`
// Without this, many servers will reject the upload with a 400.
if ($val instanceof FileParam) {
$filename = str_replace(['"', "\r", "\n"], replace: '', subject: $val->filename);

yield "; filename=\"{$filename}\"";
}

yield "\r\n";
foreach (self::writeMultipartContent($val, closing: $closing) as $chunk) {
yield $chunk;
}
}

/**
* Expands list arrays into separate multipart parts, applying the configured array key format.
*
* @param list<callable> $closing
*
* @return \Generator<string>
*/
private static function writeMultipartField(
string $boundary,
?string $key,
mixed $val,
array &$closing
): \Generator {
if (is_array($val) && array_is_list($val)) {
foreach ($val as $item) {
yield from self::writeMultipartField(boundary: $boundary, key: $key, val: $item, closing: $closing);
}
} else {
yield from self::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing);
}
}

/**
* @param bool|int|float|string|resource|\Traversable<mixed,>|array<string,mixed>|null $body
*
Expand All @@ -508,14 +546,10 @@ private static function encodeMultipartStreaming(mixed $body): array
try {
if (is_array($body) || is_object($body)) {
foreach ((array) $body as $key => $val) {
foreach (static::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing) as $chunk) {
yield $chunk;
}
yield from static::writeMultipartField(boundary: $boundary, key: $key, val: $val, closing: $closing);
}
} else {
foreach (static::writeMultipartChunk(boundary: $boundary, key: null, val: $body, closing: $closing) as $chunk) {
yield $chunk;
}
yield from static::writeMultipartField(boundary: $boundary, key: null, val: $body, closing: $closing);
}

yield "--{$boundary}--\r\n";
Expand Down
2 changes: 1 addition & 1 deletion src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
namespace CasParser;

// x-release-please-start-version
const VERSION = '0.6.2';
const VERSION = '0.6.3';
// x-release-please-end