Skip to content

Add PSR-7 server request implementation#51

Merged
loks0n merged 1 commit into
mainfrom
feat/psr7-server-request
Jul 6, 2026
Merged

Add PSR-7 server request implementation#51
loks0n merged 1 commit into
mainfrom
feat/psr7-server-request

Conversation

@loks0n

@loks0n loks0n commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add Utopia\Psr7\ServerRequest implementing PSR ServerRequestInterface
  • add PSR-17 server request factory support
  • add PSR uploaded file implementation and uploaded-file normalization
  • cover server request immutability, attributes, body parsing, and uploads

Release

  • release utopia-php/psr7 as 0.2.0 after this merges

Verification

  • bin/monorepo check psr7
  • bin/monorepo test psr7

@loks0n loks0n merged commit 5ead6d2 into main Jul 6, 2026
10 checks passed
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds ServerRequest, ServerRequest\Factory, and UploadedFile to complete the PSR-7/PSR-17 surface area of the utopia-php/psr7 package. The overall structure follows the existing Request and Message patterns well and the immutability contract is correctly upheld throughout.

  • ServerRequest and ServerRequest\Factory implement ServerRequestInterface and ServerRequestFactoryInterface correctly; all with* mutators clone before modifying.
  • UploadedFile::isFileSpec uses an OR condition that can misclassify ordinary nested arrays containing a size or error key as file specs, and normalizeFiles silently drops pre-built UploadedFileInterface entries instead of passing them through.
  • Test coverage is focused on the request layer; UploadedFile's moveTo, getStream, and multi-file normalizeFiles paths have no tests.

Confidence Score: 4/5

Safe to merge after addressing the UploadedFile normalisation issues; the request and factory code is solid.

The ServerRequest and Factory implementations are well-structured and correct. The main concerns are in UploadedFile: the OR-based isFileSpec check can silently produce a bogus UploadedFile for arrays that happen to contain a size or error key, and normalizeFiles drops pre-built UploadedFileInterface entries without warning. Neither is reachable in the normal $_FILES path, but both are surprising failure modes if the helpers are used with mixed input.

packages/psr7/src/Psr7/UploadedFile.php — the isFileSpec logic and the normalizeFiles silent-drop behaviour are the areas worth a second look before release.

Important Files Changed

Filename Overview
packages/psr7/src/Psr7/ServerRequest.php New PSR-7 ServerRequestInterface implementation extending Message; immutability, attribute, cookie, query param, and uploaded file management all look correct. The withMethod uppercases the method while the constructor does not (matching the existing Request class pattern), which is a minor inconsistency.
packages/psr7/src/Psr7/ServerRequest/Factory.php PSR-17 ServerRequestFactoryInterface implementation; correctly uppercases method and resolves string URIs, but redundantly calls withUri with the same URI to set the Host header, creating an unnecessary intermediate clone.
packages/psr7/src/Psr7/UploadedFile.php PSR-7 UploadedFile and $_FILES normalisation implementation. Two issues: isFileSpec uses overly broad OR logic that can produce a bogus UploadedFile for arrays that incidentally contain a size or error key; and normalizeFiles silently drops already-normalised UploadedFileInterface entries instead of passing them through.
packages/psr7/tests/Psr7/ServerRequestTest.php Tests cover factory creation, immutability of all mutators, and uploaded-file tree validation; UploadedFile operations (moveTo, getStream, normalizeFiles multi-file) have no coverage.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/psr7/src/Psr7/UploadedFile.php:118-123
`isFileSpec` returns `true` when ANY of the three keys are present. This means an array like `['size' => 42, 'color' => 'blue']` would be treated as a file spec and passed to `normalizeFileSpec`, which would silently create `new UploadedFile('', 42, 0, null, null)` because `tmp_name` is absent. Using AND logic (requiring at least `tmp_name` + `error`) better matches the actual `$_FILES` shape and avoids creating bogus instances for incidentally matching arrays.

```suggestion
    private static function isFileSpec(array $file): bool
    {
        return \array_key_exists('tmp_name', $file)
            && \array_key_exists('error', $file);
    }
```

### Issue 2 of 3
packages/psr7/src/Psr7/UploadedFile.php:34-39
Non-array entries are silently skipped with `continue`, so they are absent from the returned array entirely. If a caller passes an `UploadedFileInterface` instance that is already normalised (a reasonable thing to do when mixing normalised and raw `$_FILES` data), it is silently lost without any error. Passing it through to the result preserves the correct tree structure in those cases.

```suggestion
        foreach ($files as $key => $file) {
            if ($file instanceof UploadedFileInterface) {
                $normalized[$key] = $file;
                continue;
            }

            if (!\is_array($file)) {
                continue;
            }

            $normalized[$key] = self::isFileSpec($file)
```

### Issue 3 of 3
packages/psr7/src/Psr7/ServerRequest/Factory.php:27-28
The factory passes `$uri` to the `ServerRequest` constructor and then immediately calls `->withUri($uri)` with the exact same URI. The only effect of the second call is to populate the `Host` header, but it also creates an extra intermediate clone. Passing the `Host` header directly in the constructor avoids the redundant allocation.

```suggestion
        $host = $uri->getHost() !== '' ? $uri->getHost() . ($uri->getPort() !== null ? ':' . $uri->getPort() : '') : null;

        return new ServerRequest(
            strtoupper($method),
            $uri,
            $serverParams,
            headers: $host !== null ? ['Host' => $host] : [],
        );
```

Reviews (1): Last reviewed commit: "Add PSR-7 server request implementation" | Re-trigger Greptile

Comment on lines +118 to +123
private static function isFileSpec(array $file): bool
{
return \array_key_exists('tmp_name', $file)
|| \array_key_exists('error', $file)
|| \array_key_exists('size', $file);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isFileSpec returns true when ANY of the three keys are present. This means an array like ['size' => 42, 'color' => 'blue'] would be treated as a file spec and passed to normalizeFileSpec, which would silently create new UploadedFile('', 42, 0, null, null) because tmp_name is absent. Using AND logic (requiring at least tmp_name + error) better matches the actual $_FILES shape and avoids creating bogus instances for incidentally matching arrays.

Suggested change
private static function isFileSpec(array $file): bool
{
return \array_key_exists('tmp_name', $file)
|| \array_key_exists('error', $file)
|| \array_key_exists('size', $file);
}
private static function isFileSpec(array $file): bool
{
return \array_key_exists('tmp_name', $file)
&& \array_key_exists('error', $file);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/psr7/src/Psr7/UploadedFile.php
Line: 118-123

Comment:
`isFileSpec` returns `true` when ANY of the three keys are present. This means an array like `['size' => 42, 'color' => 'blue']` would be treated as a file spec and passed to `normalizeFileSpec`, which would silently create `new UploadedFile('', 42, 0, null, null)` because `tmp_name` is absent. Using AND logic (requiring at least `tmp_name` + `error`) better matches the actual `$_FILES` shape and avoids creating bogus instances for incidentally matching arrays.

```suggestion
    private static function isFileSpec(array $file): bool
    {
        return \array_key_exists('tmp_name', $file)
            && \array_key_exists('error', $file);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +34 to +39
foreach ($files as $key => $file) {
if (!\is_array($file)) {
continue;
}

$normalized[$key] = self::isFileSpec($file)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-array entries are silently skipped with continue, so they are absent from the returned array entirely. If a caller passes an UploadedFileInterface instance that is already normalised (a reasonable thing to do when mixing normalised and raw $_FILES data), it is silently lost without any error. Passing it through to the result preserves the correct tree structure in those cases.

Suggested change
foreach ($files as $key => $file) {
if (!\is_array($file)) {
continue;
}
$normalized[$key] = self::isFileSpec($file)
foreach ($files as $key => $file) {
if ($file instanceof UploadedFileInterface) {
$normalized[$key] = $file;
continue;
}
if (!\is_array($file)) {
continue;
}
$normalized[$key] = self::isFileSpec($file)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/psr7/src/Psr7/UploadedFile.php
Line: 34-39

Comment:
Non-array entries are silently skipped with `continue`, so they are absent from the returned array entirely. If a caller passes an `UploadedFileInterface` instance that is already normalised (a reasonable thing to do when mixing normalised and raw `$_FILES` data), it is silently lost without any error. Passing it through to the result preserves the correct tree structure in those cases.

```suggestion
        foreach ($files as $key => $file) {
            if ($file instanceof UploadedFileInterface) {
                $normalized[$key] = $file;
                continue;
            }

            if (!\is_array($file)) {
                continue;
            }

            $normalized[$key] = self::isFileSpec($file)
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +27 to +28
return new ServerRequest(strtoupper($method), $uri, $serverParams)
->withUri($uri);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The factory passes $uri to the ServerRequest constructor and then immediately calls ->withUri($uri) with the exact same URI. The only effect of the second call is to populate the Host header, but it also creates an extra intermediate clone. Passing the Host header directly in the constructor avoids the redundant allocation.

Suggested change
return new ServerRequest(strtoupper($method), $uri, $serverParams)
->withUri($uri);
$host = $uri->getHost() !== '' ? $uri->getHost() . ($uri->getPort() !== null ? ':' . $uri->getPort() : '') : null;
return new ServerRequest(
strtoupper($method),
$uri,
$serverParams,
headers: $host !== null ? ['Host' => $host] : [],
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/psr7/src/Psr7/ServerRequest/Factory.php
Line: 27-28

Comment:
The factory passes `$uri` to the `ServerRequest` constructor and then immediately calls `->withUri($uri)` with the exact same URI. The only effect of the second call is to populate the `Host` header, but it also creates an extra intermediate clone. Passing the `Host` header directly in the constructor avoids the redundant allocation.

```suggestion
        $host = $uri->getHost() !== '' ? $uri->getHost() . ($uri->getPort() !== null ? ':' . $uri->getPort() : '') : null;

        return new ServerRequest(
            strtoupper($method),
            $uri,
            $serverParams,
            headers: $host !== null ? ['Host' => $host] : [],
        );
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant