Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .changelog/consistent-skill-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
category: changed
type: patch
---

Synchronize the bundled local skill automatically and trust Fast Forward Composer
skill donors through `fast-forward/*`.
Resolve Changelog contributor skills from stable `0.4.x` releases without a
redundant skill allowlist.
Load the project autoloader for PHPDoc checks when DevTools is installed as a
dependency.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
/docs/ export-ignore
/README*.md export-ignore
/skills.json export-ignore
/.php-cs-fixer.dist.php export-ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ composer.lock
*.cache
TO-DO.md
/.agents/
/.dev-tools/
38 changes: 38 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* This file is part of php-fast-forward/http-message.
*
* This source file is subject to the license bundled
* with this source code in the file LICENSE.
*
* @copyright Copyright (c) 2025-2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/http-message
* @see https://github.com/php-fast-forward
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/

use FastForward\DevTools\Path\WorkingProjectPathResolver;
use PhpCsFixer\Config;
use PhpCsFixer\Finder;

require __DIR__ . '/vendor/autoload.php';

return (new Config())
->setRiskyAllowed(false)
->setFinder(Finder::create()->in(__DIR__)->exclude(WorkingProjectPathResolver::TOOLING_EXCLUDED_DIRECTORIES))
->setRules([
'phpdoc_indent' => true,
'phpdoc_order' => ['order' => ['param', 'return', 'throws']],
'phpdoc_separation' => true,
'phpdoc_trim' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_scalar' => true,
'phpdoc_types' => true,
'phpdoc_to_comment' => false,
'phpdoc_add_missing_param_annotation' => true,
]);
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
"allow-plugins": {
"ergebnis/composer-normalize": true,
"fast-forward/dev-tools": true,
"llm/skills": true,
"phpdocumentor/shim": true,
"phpro/grumphp": true,
"pyrech/composer-changelogs": true,
"llm/skills": true
"phpro/grumphp-shim": true,
"pyrech/composer-changelogs": true
},
"platform": {
"php": "8.3.0"
Expand Down
18 changes: 12 additions & 6 deletions skills.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
{
"$schema": "https://raw.githubusercontent.com/roxblnfk/skills/master/resources/skills.schema.json",
"$schema": "https://raw.githubusercontent.com/roxblnfk/skills/1.15.0/resources/skills.schema.json",
"target": ".agents/skills",
"aliases": [],
"discovery": false,
"auto-sync": true,
"vendor-sources": false,
"dependencies": {
"composer": false
"composer": {
"trusted": [
"fast-forward/*"
],
"trusted-replace": true
}
},
"sources": [
{
"from": "dir",
"path": "resources/skills"
},
{
"from": "github",
"package": "php-fast-forward/changelog",
"ref": "^0.3.0",
"skills": [
"fast-forward-changelog"
]
"ref": "^0.4.0"
}
]
}
22 changes: 16 additions & 6 deletions src/Header/Accept.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static function getBestMatch(string $acceptHeader, array $supportedTypes)
* Finds the most specific applicable range before considering its quality.
*
* @param array<int, array{type: string, q: float, specificity: int}> $preferences
* @param string $supported
*
* @return array{type: string, q: float, specificity: int}|null
*/
Expand All @@ -99,6 +100,8 @@ private static function findEffectivePreference(array $preferences, string $supp
* Parses media ranges in field order. These enum cases represent bare media
* types, so ranges requiring media parameters do not match them.
*
* @param string $header
*
* @return array<int, array{type: string, q: float, specificity: int}>
*/
private static function parseHeader(string $header): array
Expand All @@ -116,17 +119,20 @@ private static function parseHeader(string $header): array
[$name, $value] = array_pad(explode('=', $parameter, 2), 2, '');
if ('q' === strtolower(trim($name, " \t"))) {
$value = trim($value, " \t");
if (!preg_match('/^(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)$/D', $value)) {
if (! preg_match('/^(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)$/D', $value)) {
continue 2;
}

$q = (float) $value;
break;
} elseif ('' !== trim($name, " \t")) {
}

if ('' !== trim($name, " \t")) {
$hasMediaParameters = true;
}
}

if (!$hasMediaParameters) {
if (! $hasMediaParameters) {
$preferences[] = [
'type' => $type,
'q' => $q,
Expand All @@ -141,6 +147,8 @@ private static function parseHeader(string $header): array
/**
* Splits only list commas outside quoted strings, honoring quoted-pair escapes.
*
* @param string $header
*
* @return list<string>
*/
private static function splitRanges(string $header): array
Expand All @@ -149,25 +157,27 @@ private static function splitRanges(string $header): array
$start = 0;
$quoted = false;
$escaped = false;
$length = strlen($header);
$length = \strlen($header);

for ($index = 0; $index < $length; ++$index) {
$character = $header[$index];
if ($escaped) {
$escaped = false;
continue;
}

if ($quoted && '\\' === $character) {
$escaped = true;
} elseif ('"' === $character) {
$quoted = !$quoted;
} elseif (',' === $character && !$quoted) {
$quoted = ! $quoted;
} elseif (',' === $character && ! $quoted) {
$ranges[] = substr($header, $start, $index - $start);
$start = $index + 1;
}
}

$ranges[] = substr($header, $start);

return $ranges;
}

Expand Down
1 change: 1 addition & 0 deletions src/Header/Authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public static function parse(string $header): ?AuthorizationCredential
break;
}
}

if (null === $authScheme) {
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Header/TransferEncoding.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static function isChunked(string $transferEncodingHeader): bool
}

$codings = array_map(
static fn (string $coding): string => trim($coding, " \t"),
static fn(string $coding): string => trim($coding, " \t"),
explode(',', strtolower($transferEncodingHeader)),
);

Expand Down
14 changes: 9 additions & 5 deletions src/JsonResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
namespace FastForward\Http\Message;

use FastForward\Http\Message\Header\ContentType;
use JsonException;
use Nyholm\Psr7\Response;
use RuntimeException;

/**
* Class JsonResponse.
Expand Down Expand Up @@ -60,10 +62,10 @@ public function __construct(mixed $payload = [], string $charset = 'utf-8', arra
* Invalid JSON throws JsonException; unreadable, detached or non-seekable
* generic streams throw RuntimeException rather than being consumed.
*
* @throws \JsonException
* @throws \RuntimeException
*
* @return mixed the decoded JSON payload
*
* @throws JsonException
* @throws RuntimeException
*/
public function getPayload(): mixed
{
Expand All @@ -72,13 +74,14 @@ public function getPayload(): mixed
return $body->getPayload();
}

if (!$body->isReadable() || !$body->isSeekable()) {
throw new \RuntimeException('A generic JSON body must be readable and seekable to retrieve its payload.');
if (! $body->isReadable() || ! $body->isSeekable()) {
throw new RuntimeException('A generic JSON body must be readable and seekable to retrieve its payload.');
}

$position = $body->tell();
try {
$body->rewind();

return json_decode($body->getContents(), true, 512, \JSON_THROW_ON_ERROR);
} finally {
$body->seek($position);
Expand All @@ -99,6 +102,7 @@ public function getPayload(): mixed
public function withPayload(mixed $payload): self
{
$body = $this->getBody();

return $this->withBody(
$body instanceof PayloadStreamInterface ? $body->withPayload($payload) : new JsonStream($payload)
);
Expand Down
40 changes: 30 additions & 10 deletions src/RequestMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,54 @@
*/
enum RequestMethod: string
{
/** The HEAD method requests the headers for a given resource without the response body. */
/**
* The HEAD method requests the headers for a given resource without the response body.
*/
case Head = 'HEAD';

/** The GET method requests a representation of the specified resource. It MUST NOT have side-effects. */
/**
* The GET method requests a representation of the specified resource. It MUST NOT have side-effects.
*/
case Get = 'GET';

/** The POST method submits data to be processed, often causing a change in state or side-effects. */
/**
* The POST method submits data to be processed, often causing a change in state or side-effects.
*/
case Post = 'POST';

/** The PUT method replaces the target resource with the request payload. */
/**
* The PUT method replaces the target resource with the request payload.
*/
case Put = 'PUT';

/** The PATCH method applies partial modifications to the target resource. */
/**
* The PATCH method applies partial modifications to the target resource.
*/
case Patch = 'PATCH';

/** The DELETE method removes the specified resource. */
/**
* The DELETE method removes the specified resource.
*/
case Delete = 'DELETE';

/** The PURGE method requests that a cached resource be removed, often used with proxy servers. */
/**
* The PURGE method requests that a cached resource be removed, often used with proxy servers.
*/
case Purge = 'PURGE';

/** The OPTIONS method describes the communication options for the target resource. */
/**
* The OPTIONS method describes the communication options for the target resource.
*/
case Options = 'OPTIONS';

/** The TRACE method performs a message loop-back test along the path to the target resource. */
/**
* The TRACE method performs a message loop-back test along the path to the target resource.
*/
case Trace = 'TRACE';

/** The CONNECT method establishes a tunnel to the target resource, often used with HTTPS proxies. */
/**
* The CONNECT method establishes a tunnel to the target resource, often used with HTTPS proxies.
*/
case Connect = 'CONNECT';

/**
Expand Down
Loading
Loading